'use client' import type { FC } from 'react' import useSWR from 'swr' import { useTranslation } from 'react-i18next' import React, { useCallback, useEffect, useState } from 'react' import { setAutoFreeze } from 'immer' import { useBoolean } from 'ahooks' import { useContext } from 'use-context-selector' import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api' import FormattingChanged from '../base/warning-mask/formatting-changed' import GroupName from '../base/group-name' import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset' import DebugWithMultipleModel from './debug-with-multiple-model' import DebugWithSingleModel from './debug-with-single-model' import type { DebugWithSingleModelRefType } from './debug-with-single-model' import type { ModelAndParameter } from './types' import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, } from './types' import { AppType, ModelModeType, TransferMethod } from '@/types/app' import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel' import ConfigContext from '@/context/debug-configuration' import { ToastContext } from '@/app/components/base/toast' import { sendCompletionMessage } from '@/service/debug' import Button from '@/app/components/base/button' import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app' import { promptVariablesToUserInputsForm } from '@/utils/model-config' import TextGeneration from '@/app/components/app/text-generate/item' import { IS_CE_EDITION } from '@/config' import type { Inputs } from '@/models/debug' import { fetchFileUploadConfig } from '@/service/common' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import { Plus } from '@/app/components/base/icons/src/vender/line/general' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useProviderContext } from '@/context/provider-context' type IDebug = { hasSetAPIKEY: boolean onSetting: () => void inputs: Inputs modelParameterParams: Pick debugWithMultipleModel: boolean multipleModelConfigs: ModelAndParameter[] onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void } const Debug: FC = ({ hasSetAPIKEY = true, onSetting, inputs, modelParameterParams, debugWithMultipleModel, multipleModelConfigs, onMultipleModelConfigsChange, }) => { const { t } = useTranslation() const { appId, mode, modelModeType, hasSetBlockStatus, isAdvancedMode, promptMode, chatPromptConfig, completionPromptConfig, introduction, suggestedQuestionsAfterAnswerConfig, speechToTextConfig, textToSpeechConfig, citationConfig, moderationConfig, moreLikeThisConfig, formattingChanged, setFormattingChanged, dataSets, modelConfig, completionParams, hasSetContextVar, datasetConfigs, visionConfig, setVisionConfig, } = useContext(ConfigContext) const { eventEmitter } = useEventEmitterContextContext() const { data: text2speechDefaultModel } = useDefaultModel(5) const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) useEffect(() => { setAutoFreeze(false) return () => { setAutoFreeze(true) } }, []) const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false) const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false) const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false) useEffect(() => { if (formattingChanged) setIsShowFormattingChangeConfirm(true) }, [formattingChanged]) const debugWithSingleModelRef = React.useRef(null) const handleClearConversation = () => { debugWithSingleModelRef.current?.handleRestart() } const clearConversation = async () => { if (debugWithMultipleModel) { eventEmitter?.emit({ type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, } as any) return } handleClearConversation() } const handleConfirm = () => { clearConversation() setIsShowFormattingChangeConfirm(false) setFormattingChanged(false) } const handleCancel = () => { setIsShowFormattingChangeConfirm(false) setFormattingChanged(false) } const { notify } = useContext(ToastContext) const logError = useCallback((message: string) => { notify({ type: 'error', message, duration: 3000 }) }, [notify]) const [completionFiles, setCompletionFiles] = useState([]) const checkCanSend = useCallback(() => { if (isAdvancedMode && mode === AppType.chat) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 }) return false } if (!hasSetBlockStatus.query) { notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 }) return false } } } let hasEmptyInput = '' const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required, type }) => { if (type !== 'string' && type !== 'paragraph' && type !== 'select') return false const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) return res }) // compatible with old version // debugger requiredVars.forEach(({ key, name }) => { if (hasEmptyInput) return if (!inputs[key]) hasEmptyInput = name }) if (hasEmptyInput) { logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput })) return false } if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) return false } return !hasEmptyInput }, [ completionFiles, hasSetBlockStatus.history, hasSetBlockStatus.query, inputs, isAdvancedMode, mode, modelConfig.configs.prompt_variables, t, logError, notify, modelModeType, ]) const [completionRes, setCompletionRes] = useState('') const [messageId, setMessageId] = useState(null) const sendTextCompletion = async () => { if (isResponding) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) return false } if (dataSets.length > 0 && !hasSetContextVar) { setShowCannotQueryDataset(true) return true } if (!checkCanSend()) return const postDatasets = dataSets.map(({ id }) => ({ dataset: { enabled: true, id, }, })) const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key const postModelConfig: BackendModelConfig = { pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '', prompt_type: promptMode, chat_prompt_config: {}, completion_prompt_config: {}, user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables), dataset_query_variable: contextVar || '', opening_statement: introduction, suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig, speech_to_text: speechToTextConfig, retriever_resource: citationConfig, sensitive_word_avoidance: moderationConfig, more_like_this: moreLikeThisConfig, model: { provider: modelConfig.provider, name: modelConfig.model_id, mode: modelConfig.mode, completion_params: completionParams as any, }, text_to_speech: { enabled: false, voice: '', language: '', }, agent_mode: { enabled: false, tools: [], }, dataset_configs: { ...datasetConfigs, datasets: { datasets: [...postDatasets], } as any, }, file_upload: { image: visionConfig, }, } if (isAdvancedMode) { postModelConfig.chat_prompt_config = chatPromptConfig postModelConfig.completion_prompt_config = completionPromptConfig } const data: Record = { inputs, model_config: postModelConfig, } if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) { data.files = completionFiles.map((item) => { if (item.transfer_method === TransferMethod.local_file) { return { ...item, url: '', } } return item }) } setCompletionRes('') setMessageId('') let res: string[] = [] setRespondingTrue() sendCompletionMessage(appId, data, { onData: (data: string, _isFirstMessage: boolean, { messageId }) => { res.push(data) setCompletionRes(res.join('')) setMessageId(messageId) }, onMessageReplace: (messageReplace) => { res = [messageReplace.answer] setCompletionRes(res.join('')) }, onCompleted() { setRespondingFalse() }, onError() { setRespondingFalse() }, }) } const handleSendTextCompletion = () => { if (debugWithMultipleModel) { eventEmitter?.emit({ type: APP_CHAT_WITH_MULTIPLE_MODEL, payload: { message: '', files: completionFiles, }, } as any) return } sendTextCompletion() } const varList = modelConfig.configs.prompt_variables.map((item: any) => { return { label: item.key, value: inputs[item.key], } }) const { textGenerationModelList } = useProviderContext() const handleChangeToSingleModel = (item: ModelAndParameter) => { const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === item.provider) const currentModel = currentProvider?.models.find(model => model.model === item.model) modelParameterParams.setModel({ modelId: item.model, provider: item.provider, mode: currentModel?.model_properties.mode as string, features: currentModel?.features, }) modelParameterParams.onCompletionParamsChange(item.parameters) onMultipleModelConfigsChange( false, [], ) } const handleVisionConfigInMultipleModel = () => { if (debugWithMultipleModel && mode) { const supportedVision = multipleModelConfigs.some((modelConfig) => { const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider) const currentModel = currentProvider?.models.find(model => model.model === modelConfig.model) return currentModel?.features?.includes(ModelFeatureEnum.vision) }) if (supportedVision) { setVisionConfig({ ...visionConfig, enabled: true, }, true) } else { setVisionConfig({ ...visionConfig, enabled: false, }, true) } } } useEffect(() => { handleVisionConfigInMultipleModel() }, [multipleModelConfigs, mode]) return ( <>
{t('appDebug.inputs.title')}
{ debugWithMultipleModel ? ( <>
) : null } {mode === 'chat' && ( )}
{ debugWithMultipleModel && (
) } { !debugWithMultipleModel && (
{/* Chat */} {mode === AppType.chat && (
)} {/* Text Generation */} {mode === AppType.completion && (
{(completionRes || isResponding) && ( { }} supportAnnotation appId={appId} varList={varList} /> )}
)} {isShowCannotQueryDataset && ( setShowCannotQueryDataset(false)} /> )}
) } {isShowFormattingChangeConfirm && ( )} {!hasSetAPIKEY && ()} ) } export default React.memo(Debug)