From 1d027fa0650bd17fe943c3d82a1a6aa4d9527cde Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Thu, 26 Sep 2024 11:06:51 +0800 Subject: [PATCH] fix: chat check inputs form --- .../chat/chat-with-history/chat-wrapper.tsx | 4 +- .../base/chat/chat/chat-input-area/index.tsx | 17 ++++-- .../base/chat/chat/check-input-forms-hooks.ts | 51 ++++++++++++++++ web/app/components/base/chat/chat/hooks.ts | 60 +++---------------- web/app/components/base/chat/chat/index.tsx | 10 +++- web/app/components/base/chat/chat/type.ts | 9 +++ web/app/components/base/chat/chat/utils.ts | 16 +++++ .../file-uploader-in-chat-input/file-item.tsx | 13 ++-- .../hooks/use-check-start-node-form.ts | 39 +----------- .../panel/debug-and-preview/chat-wrapper.tsx | 10 ++-- .../workflow/panel/debug-and-preview/hooks.ts | 12 ++-- 11 files changed, 125 insertions(+), 116 deletions(-) create mode 100644 web/app/components/base/chat/chat/check-input-forms-hooks.ts create mode 100644 web/app/components/base/chat/chat/utils.ts diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 469cdc8f2a..b2bedb785f 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -55,7 +55,7 @@ const ChatWrapper = () => { appConfig, { inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, - promptVariables: inputsForms, + inputsForm: inputsForms, }, appPrevChatList, taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), @@ -165,6 +165,8 @@ const ChatWrapper = () => { chatFooterClassName='pb-4' chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`} onSend={doSend} + inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} + inputsForm={inputsForms} onRegenerate={doRegenerate} onStopResponding={handleStop} chatNode={chatNode} diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 0f3f135a93..8ce4c8c7a3 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -11,6 +11,8 @@ import type { OnSend, } from '../../types' import type { Theme } from '../../embedded-chatbot/theme/theme-context' +import type { InputForm } from '../type' +import { useCheckInputsForms } from '../check-input-forms-hooks' import { useTextAreaHeight } from './hooks' import Operation from './operation' import cn from '@/utils/classnames' @@ -18,7 +20,7 @@ import { FileListInChatInput } from '@/app/components/base/file-uploader' import { useFile } from '@/app/components/base/file-uploader/hooks' import { FileContextProvider, - useStore, + useFileStore, } from '@/app/components/base/file-uploader/store' import VoiceInput from '@/app/components/base/voice-input' import { useToastContext } from '@/app/components/base/toast' @@ -34,7 +36,8 @@ type ChatInputAreaProps = { visionConfig?: FileUpload speechToTextConfig?: EnableType onSend?: OnSend - onSendCheck?: () => boolean + inputs?: Record + inputsForm?: InputForm[] theme?: Theme | null } const ChatInputArea = ({ @@ -45,7 +48,8 @@ const ChatInputArea = ({ visionConfig, speechToTextConfig = { enabled: true }, onSend, - onSendCheck = () => true, + inputs = {}, + inputsForm = [], // theme, }: ChatInputAreaProps) => { const { t } = useTranslation() @@ -61,8 +65,7 @@ const ChatInputArea = ({ const [query, setQuery] = useState('') const isUseInputMethod = useRef(false) const [showVoiceInput, setShowVoiceInput] = useState(false) - const files = useStore(s => s.files) - const setFiles = useStore(s => s.setFiles) + const filesStore = useFileStore() const { handleDragFileEnter, handleDragFileLeave, @@ -71,9 +74,11 @@ const ChatInputArea = ({ handleClipboardPasteFile, isDragActive, } = useFile(visionConfig!) + const { checkInputsForm } = useCheckInputsForms() const handleSend = () => { if (onSend) { + const { files, setFiles } = filesStore.getState() if (files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) return @@ -82,7 +87,7 @@ const ChatInputArea = ({ notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') }) return } - if (onSendCheck()) { + if (checkInputsForm(inputs, inputsForm)) { onSend(query, files) setQuery('') setFiles([]) diff --git a/web/app/components/base/chat/chat/check-input-forms-hooks.ts b/web/app/components/base/chat/chat/check-input-forms-hooks.ts new file mode 100644 index 0000000000..bf6dd6f26a --- /dev/null +++ b/web/app/components/base/chat/chat/check-input-forms-hooks.ts @@ -0,0 +1,51 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import type { InputForm } from './type' +import { useToastContext } from '@/app/components/base/toast' +import { InputVarType } from '@/app/components/workflow/types' +import { TransferMethod } from '@/types/app' + +export const useCheckInputsForms = () => { + const { t } = useTranslation() + const { notify } = useToastContext() + + const checkInputsForm = useCallback((inputs: Record, inputsForm: InputForm[]) => { + let hasEmptyInput = '' + let fileIsUploading = false + const requiredVars = inputsForm.filter(({ required }) => required) + + if (requiredVars?.length) { + requiredVars.forEach(({ variable, label, type }) => { + if (hasEmptyInput) + return + + if (!inputs[variable]) + hasEmptyInput = label as string + + if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputs[variable]) { + const files = inputs[variable] + if (Array.isArray(files)) + fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId) + else + fileIsUploading = files.transfer_method === TransferMethod.local_file && !files.uploadedId + } + }) + } + + if (hasEmptyInput) { + notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) + return false + } + + if (fileIsUploading) { + notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) + return + } + + return true + }, [notify, t]) + + return { + checkInputsForm, + } +} diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 78c30a0d05..719433c31e 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -12,12 +12,12 @@ import type { ChatConfig, ChatItem, Inputs, - PromptVariable, } from '../types' +import type { InputForm } from './type' +import { processOpeningStatement } from './utils' import { TransferMethod } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' import { ssePost } from '@/service/base' -import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils' import type { Annotation } from '@/models/log' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import useTimestamp from '@/hooks/use-timestamp' @@ -33,50 +33,11 @@ type SendCallback = { isPublicAPI?: boolean } -export const useCheckPromptVariables = () => { - const { t } = useTranslation() - const { notify } = useToastContext() - - const checkPromptVariables = useCallback((promptVariablesConfig: { - inputs: Inputs - promptVariables: PromptVariable[] - }) => { - const { - promptVariables, - inputs, - } = promptVariablesConfig - let hasEmptyInput = '' - const requiredVars = promptVariables.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 - }) - - if (requiredVars?.length) { - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs[key]) - hasEmptyInput = name - }) - } - - if (hasEmptyInput) { - notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) - return false - } - }, [notify, t]) - - return checkPromptVariables -} - export const useChat = ( config?: ChatConfig, - promptVariablesConfig?: { + formSettings?: { inputs: Inputs - promptVariables: PromptVariable[] + inputsForm: InputForm[] }, prevChatList?: ChatItem[], stopChat?: (taskId: string) => void, @@ -94,7 +55,6 @@ export const useChat = ( const [suggestedQuestions, setSuggestQuestions] = useState([]) const conversationMessagesAbortControllerRef = useRef(null) const suggestedQuestionsAbortControllerRef = useRef(null) - const checkPromptVariables = useCheckPromptVariables() const params = useParams() const pathname = usePathname() useEffect(() => { @@ -114,8 +74,8 @@ export const useChat = ( }, []) const getIntroduction = useCallback((str: string) => { - return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) - }, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) + return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || []) + }, [formSettings?.inputs, formSettings?.inputsForm]) useEffect(() => { if (config?.opening_statement) { handleUpdateChatList(produce(chatListRef.current, (draft) => { @@ -216,9 +176,6 @@ export const useChat = ( return false } - if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables) - checkPromptVariables(promptVariablesConfig) - const questionId = `question-${Date.now()}` const questionItem = { id: questionId, @@ -575,15 +532,16 @@ export const useChat = ( }) return true }, [ - checkPromptVariables, config?.suggested_questions_after_answer, updateCurrentQA, t, notify, - promptVariablesConfig, handleUpdateChatList, handleResponding, formatTime, + params.token, + params.appId, + pathname, ]) const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index c4939ee528..742632a1ad 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -25,6 +25,7 @@ import Answer from './answer' import ChatInputArea from './chat-input-area' import TryToAsk from './try-to-ask' import { ChatContextProvider } from './context' +import type { InputForm } from './type' import cn from '@/utils/classnames' import type { Emoji } from '@/app/components/tools/types' import Button from '@/app/components/base/button' @@ -43,7 +44,8 @@ export type ChatProps = { onStopResponding?: () => void noChatInput?: boolean onSend?: OnSend - onSendCheck?: () => boolean + inputs?: Record + inputsForm?: InputForm[] onRegenerate?: OnRegenerate chatContainerClassName?: string chatContainerInnerClassName?: string @@ -73,7 +75,8 @@ const Chat: FC = ({ appData, config, onSend, - onSendCheck, + inputs, + inputsForm, onRegenerate, chatList, isResponding, @@ -283,7 +286,8 @@ const Chat: FC = ({ visionConfig={config?.file_upload} speechToTextConfig={config?.speech_to_text} onSend={onSend} - onSendCheck={onSendCheck} + inputs={inputs} + inputsForm={inputsForm} theme={themeBuilder?.theme} /> ) diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts index 4bb8b306ba..cbafd11d6f 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -2,6 +2,7 @@ import type { TypeWithI18N } from '@/app/components/header/account-setting/model import type { Annotation, MessageRating } from '@/models/log' import type { VisionFile } from '@/types/app' import type { FileEntity } from '@/app/components/base/file-uploader/types' +import type { InputVarType } from '@/app/components/workflow/types' export type MessageMore = { time: string @@ -130,3 +131,11 @@ export type AnnotationReply = { annotation_id: string annotation_author_name: string } + +export type InputForm = { + type: InputVarType + label: string + variable: any + required: boolean + [key: string]: any +} diff --git a/web/app/components/base/chat/chat/utils.ts b/web/app/components/base/chat/chat/utils.ts new file mode 100644 index 0000000000..54ada220fc --- /dev/null +++ b/web/app/components/base/chat/chat/utils.ts @@ -0,0 +1,16 @@ +import type { InputForm } from './type' + +export const processOpeningStatement = (openingStatement: string, inputs: Record, inputsForm: InputForm[]) => { + if (!openingStatement) + return openingStatement + + return openingStatement.replace(/\{\{([^}]+)\}\}/g, (match, key) => { + const name = inputs[key] + if (name) { // has set value + return name + } + + const valueObj = inputsForm.find(v => v.variable === key) + return valueObj ? `{{${valueObj.label}}}` : match + }) +} diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx index d4b30e5ca4..148228a2e2 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx @@ -74,12 +74,13 @@ const FileItem = ({ { showDownloadAction && ( - window.open(file.url, '_blank')} - > - - + + + + + ) } { diff --git a/web/app/components/workflow/hooks/use-check-start-node-form.ts b/web/app/components/workflow/hooks/use-check-start-node-form.ts index 946f7a0f36..eacf6851a9 100644 --- a/web/app/components/workflow/hooks/use-check-start-node-form.ts +++ b/web/app/components/workflow/hooks/use-check-start-node-form.ts @@ -1,48 +1,14 @@ import { useCallback } from 'react' -import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' -import { useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum, InputVarType, } from '@/app/components/workflow/types' -import { useToastContext } from '@/app/components/base/toast' import type { InputVar } from '@/app/components/workflow/types' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' export const useCheckStartNodeForm = () => { - const { t } = useTranslation() const storeApi = useStoreApi() - const workflowStore = useWorkflowStore() - const { notify } = useToastContext() - - const checkStartNodeForm = useCallback(() => { - const { getNodes } = storeApi.getState() - const nodes = getNodes() - const startNode = nodes.find(node => node.data.type === BlockEnum.Start) - const variables: InputVar[] = startNode?.data.variables || [] - const inputs = workflowStore.getState().inputs - - let hasEmptyInput = '' - const requiredVars = variables.filter(({ required }) => required) - - if (requiredVars?.length) { - requiredVars.forEach(({ variable, label }) => { - if (hasEmptyInput) - return - - if (!inputs[variable]) - hasEmptyInput = label as string - }) - } - - if (hasEmptyInput) { - notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) - return false - } - - return true - }, [storeApi, workflowStore, notify, t]) const getProcessedInputs = useCallback((inputs: Record) => { const { getNodes } = storeApi.getState() @@ -53,10 +19,10 @@ export const useCheckStartNodeForm = () => { const processedInputs = { ...inputs } variables.forEach((variable) => { - if (variable.type === InputVarType.multiFiles) + if (variable.type === InputVarType.multiFiles && inputs[variable.variable]) processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable]) - if (variable.type === InputVarType.singleFile) + if (variable.type === InputVarType.singleFile && inputs[variable.variable]) processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0] }) @@ -64,7 +30,6 @@ export const useCheckStartNodeForm = () => { }, [storeApi]) return { - checkStartNodeForm, getProcessedInputs, } } diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index 06d67a13c0..d835df7180 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -62,10 +62,7 @@ const ChatWrapper = forwardRef(({ } }, [features.opening, features.suggested, features.text2speech, features.speech2text, features.citation, features.moderation, features.file]) const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel) - const { - checkStartNodeForm, - getProcessedInputs, - } = useCheckStartNodeForm() + const { getProcessedInputs } = useCheckStartNodeForm() const { conversationId, @@ -81,7 +78,7 @@ const ChatWrapper = forwardRef(({ config, { inputs, - promptVariables: (startVariables as any) || [], + inputsForm: (startVariables || []) as any, }, [], taskId => stopChatMessageResponding(appDetail!.id, taskId), @@ -146,7 +143,8 @@ const ChatWrapper = forwardRef(({ showFeatureBar onFeatureBarClick={setShowFeaturesPanel} onSend={doSend} - onSendCheck={checkStartNodeForm} + inputs={inputs} + inputsForm={(startVariables || []) as any} onRegenerate={doRegenerate} onStopResponding={handleStop} chatNode={( diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 3a5a278ba0..92d5d18657 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -12,11 +12,11 @@ import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' import type { ChatItem, Inputs, - PromptVariable, } from '@/app/components/base/chat/types' +import type { InputForm } from '@/app/components/base/chat/chat/type' +import { processOpeningStatement } from '@/app/components/base/chat/chat/utils' import { useToastContext } from '@/app/components/base/toast' import { TransferMethod } from '@/types/app' -import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils' import { getProcessedFiles, getProcessedFilesFromResponse, @@ -29,9 +29,9 @@ type SendCallback = { } export const useChat = ( config: any, - promptVariablesConfig?: { + formSettings?: { inputs: Inputs - promptVariables: PromptVariable[] + inputsForm: InputForm[] }, prevChatList?: ChatItem[], stopChat?: (taskId: string) => void, @@ -67,8 +67,8 @@ export const useChat = ( }, []) const getIntroduction = useCallback((str: string) => { - return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) - }, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) + return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || []) + }, [formSettings?.inputs, formSettings?.inputsForm]) useEffect(() => { if (config?.opening_statement) { handleUpdateChatList(produce(chatListRef.current, (draft) => {