From a7d53abba902b212983206878f2b0d5e9e66eb33 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Thu, 26 Sep 2024 13:47:36 +0800 Subject: [PATCH] webapp chat embedded chat --- .../chat-with-history/config-panel/form.tsx | 19 +++++++++- .../base/chat/chat-with-history/hooks.tsx | 16 ++++++++- web/app/components/base/chat/chat/hooks.ts | 16 +++++++-- web/app/components/base/chat/chat/utils.ts | 16 +++++++++ .../chat/embedded-chatbot/chat-wrapper.tsx | 4 ++- .../embedded-chatbot/config-panel/form.tsx | 31 +++++++++++++++- .../base/chat/embedded-chatbot/hooks.tsx | 14 ++++++++ web/app/components/workflow/hooks/index.ts | 1 - .../hooks/use-check-start-node-form.ts | 35 ------------------- .../panel/debug-and-preview/chat-wrapper.tsx | 6 ++-- .../workflow/panel/inputs-panel.tsx | 8 ++--- web/app/components/workflow/types.ts | 1 + web/types/app.ts | 3 +- 13 files changed, 117 insertions(+), 53 deletions(-) delete mode 100644 web/app/components/workflow/hooks/use-check-start-node-form.ts diff --git a/web/app/components/base/chat/chat-with-history/config-panel/form.tsx b/web/app/components/base/chat/chat-with-history/config-panel/form.tsx index 39b2426a5f..cd375a887e 100644 --- a/web/app/components/base/chat/chat-with-history/config-panel/form.tsx +++ b/web/app/components/base/chat/chat-with-history/config-panel/form.tsx @@ -3,17 +3,20 @@ import { useTranslation } from 'react-i18next' import { useChatWithHistoryContext } from '../context' import Input from './form-input' import { PortalSelect } from '@/app/components/base/select' +import { InputVarType } from '@/app/components/workflow/types' +import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' const Form = () => { const { t } = useTranslation() const { + appParams, inputsForms, newConversationInputs, handleNewConversationInputsChange, isMobile, } = useChatWithHistoryContext() - const handleFormChange = useCallback((variable: string, value: string) => { + const handleFormChange = useCallback((variable: string, value: any) => { handleNewConversationInputsChange({ ...newConversationInputs, [variable]: value, @@ -48,6 +51,20 @@ const Form = () => { /> ) } + if (form.type === InputVarType.multiFiles) { + return ( + handleFormChange(variable, files)} + fileConfig={{ + allowed_file_types: appParams?.file_upload?.allowed_file_types, + allowed_file_extensions: appParams?.file_upload?.allowed_file_extensions, + allowed_file_upload_methods: appParams?.file_upload?.allowed_file_upload_methods, + number_limits: appParams?.file_upload?.number_limits, + }} + /> + ) + } return ( { setNewConversationInputs(newInputs) }, []) const inputsForms = useMemo(() => { - return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => { + return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => { if (item.paragraph) { return { ...item.paragraph, @@ -147,6 +147,20 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } } + if (item['file-list']) { + return { + ...item['file-list'], + type: 'file-list', + } + } + + if (item.file) { + return { + ...item.file, + type: 'file', + } + } + return { ...item['text-input'], type: 'text-input', diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 719433c31e..d392f807ee 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -6,6 +6,7 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { produce, setAutoFreeze } from 'immer' +import { uniqBy } from 'lodash-es' import { useParams, usePathname } from 'next/navigation' import { v4 as uuidV4 } from 'uuid' import type { @@ -14,7 +15,10 @@ import type { Inputs, } from '../types' import type { InputForm } from './type' -import { processOpeningStatement } from './utils' +import { + getProcessedInputs, + processOpeningStatement, +} from './utils' import { TransferMethod } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' import { ssePost } from '@/service/base' @@ -23,7 +27,10 @@ import { WorkflowRunningStatus } from '@/app/components/workflow/types' import useTimestamp from '@/hooks/use-timestamp' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' import type { FileEntity } from '@/app/components/base/file-uploader/types' -import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' +import { + getProcessedFiles, + getProcessedFilesFromResponse, +} from '@/app/components/base/file-uploader/utils' type GetAbortController = (abortController: AbortController) => void type SendCallback = { @@ -206,12 +213,13 @@ export const useChat = ( handleResponding(true) hasStopResponded.current = false - const { query, files, ...restData } = data + const { query, files, inputs, ...restData } = data const bodyParams = { response_mode: 'streaming', conversation_id: conversationId.current, files: getProcessedFiles(files || []), query, + inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []), ...restData, } if (bodyParams?.files?.length) { @@ -512,6 +520,8 @@ export const useChat = ( return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id) }) responseItem.workflowProcess!.tracing[currentIndex] = data as any + const processedFilesFromResponse = getProcessedFilesFromResponse(data.files || []) + responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id') handleUpdateChatList(produce(chatListRef.current, (draft) => { const currentIndex = draft.findIndex(item => item.id === responseItem.id) draft[currentIndex] = { diff --git a/web/app/components/base/chat/chat/utils.ts b/web/app/components/base/chat/chat/utils.ts index 54ada220fc..34d00afe33 100644 --- a/web/app/components/base/chat/chat/utils.ts +++ b/web/app/components/base/chat/chat/utils.ts @@ -1,4 +1,6 @@ import type { InputForm } from './type' +import { InputVarType } from '@/app/components/workflow/types' +import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' export const processOpeningStatement = (openingStatement: string, inputs: Record, inputsForm: InputForm[]) => { if (!openingStatement) @@ -14,3 +16,17 @@ export const processOpeningStatement = (openingStatement: string, inputs: Record return valueObj ? `{{${valueObj.label}}}` : match }) } + +export const getProcessedInputs = (inputs: Record, inputsForm: InputForm[]) => { + const processedInputs = { ...inputs } + + inputsForm.forEach((item) => { + if (item.type === InputVarType.multiFiles && inputs[item.variable]) + processedInputs[item.variable] = getProcessedFiles(inputs[item.variable]) + + if (item.type === InputVarType.singleFile && inputs[item.variable]) + processedInputs[item.variable] = getProcessedFiles([inputs[item.variable]])[0] + }) + + return processedInputs +} diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index b97c940eec..f3c8513213 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -58,7 +58,7 @@ const ChatWrapper = () => { appConfig, { inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, - promptVariables: inputsForms, + inputsForm: inputsForms, }, appPrevChatList, taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), @@ -159,6 +159,8 @@ const ChatWrapper = () => { chatFooterClassName='pb-4' chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', 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/embedded-chatbot/config-panel/form.tsx b/web/app/components/base/chat/embedded-chatbot/config-panel/form.tsx index d34e94a495..15adf9f8a6 100644 --- a/web/app/components/base/chat/embedded-chatbot/config-panel/form.tsx +++ b/web/app/components/base/chat/embedded-chatbot/config-panel/form.tsx @@ -3,17 +3,20 @@ import { useTranslation } from 'react-i18next' import { useEmbeddedChatbotContext } from '../context' import Input from './form-input' import { PortalSelect } from '@/app/components/base/select' +import { InputVarType } from '@/app/components/workflow/types' +import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' const Form = () => { const { t } = useTranslation() const { + appParams, inputsForms, newConversationInputs, handleNewConversationInputsChange, isMobile, } = useEmbeddedChatbotContext() - const handleFormChange = useCallback((variable: string, value: string) => { + const handleFormChange = useCallback((variable: string, value: any) => { handleNewConversationInputsChange({ ...newConversationInputs, [variable]: value, @@ -49,6 +52,32 @@ const Form = () => { ) } + if (form.type === 'number') { + return ( + handleFormChange(variable, e.target.value)} + placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`} + /> + ) + } + if (form.type === InputVarType.multiFiles) { + return ( + handleFormChange(variable, files)} + fileConfig={{ + allowed_file_types: appParams?.file_upload?.allowed_file_types, + allowed_file_extensions: appParams?.file_upload?.allowed_file_extensions, + allowed_file_upload_methods: appParams?.file_upload?.allowed_file_upload_methods, + number_limits: appParams?.file_upload?.number_limits, + }} + /> + ) + } + return ( { } } + if (item['file-list']) { + return { + ...item['file-list'], + type: 'file-list', + } + } + + if (item.file) { + return { + ...item.file, + type: 'file', + } + } + let value = initInputs[item['text-input'].variable] if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) value = value.slice(0, item['text-input'].max_length) diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index afea0116ea..463e9b3271 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -16,4 +16,3 @@ export * from './use-workflow-variables' export * from './use-shortcuts' export * from './use-workflow-interactions' export * from './use-workflow-mode' -export * from './use-check-start-node-form' 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 deleted file mode 100644 index eacf6851a9..0000000000 --- a/web/app/components/workflow/hooks/use-check-start-node-form.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useCallback } from 'react' -import { useStoreApi } from 'reactflow' -import { - BlockEnum, - InputVarType, -} from '@/app/components/workflow/types' -import type { InputVar } from '@/app/components/workflow/types' -import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' - -export const useCheckStartNodeForm = () => { - const storeApi = useStoreApi() - - const getProcessedInputs = useCallback((inputs: Record) => { - const { getNodes } = storeApi.getState() - const nodes = getNodes() - const startNode = nodes.find(node => node.data.type === BlockEnum.Start) - const variables: InputVar[] = startNode?.data.variables || [] - - const processedInputs = { ...inputs } - - variables.forEach((variable) => { - if (variable.type === InputVarType.multiFiles && inputs[variable.variable]) - processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable]) - - if (variable.type === InputVarType.singleFile && inputs[variable.variable]) - processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0] - }) - - return processedInputs - }, [storeApi]) - - return { - 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 d835df7180..42c30df7cf 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 @@ -12,7 +12,6 @@ import { useStore, useWorkflowStore, } from '../../store' -import { useCheckStartNodeForm } from '../../hooks' import type { StartNodeType } from '../../nodes/start/types' import Empty from './empty' import UserInput from './user-input' @@ -62,7 +61,6 @@ const ChatWrapper = forwardRef(({ } }, [features.opening, features.suggested, features.text2speech, features.speech2text, features.citation, features.moderation, features.file]) const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel) - const { getProcessedInputs } = useCheckStartNodeForm() const { conversationId, @@ -89,7 +87,7 @@ const ChatWrapper = forwardRef(({ { query, files, - inputs: getProcessedInputs(workflowStore.getState().inputs), + inputs: workflowStore.getState().inputs, conversation_id: conversationId, parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null, }, @@ -97,7 +95,7 @@ const ChatWrapper = forwardRef(({ onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController), }, ) - }, [chatListRef, conversationId, handleSend, workflowStore, appDetail, getProcessedInputs]) + }, [chatListRef, conversationId, handleSend, workflowStore, appDetail]) const doRegenerate = useCallback((chatItem: ChatItem) => { const index = chatList.findIndex(item => item.id === chatItem.id) diff --git a/web/app/components/workflow/panel/inputs-panel.tsx b/web/app/components/workflow/panel/inputs-panel.tsx index 16ed730dc6..ac83e69ee0 100644 --- a/web/app/components/workflow/panel/inputs-panel.tsx +++ b/web/app/components/workflow/panel/inputs-panel.tsx @@ -15,7 +15,7 @@ import { useStore, useWorkflowStore, } from '../store' -import { useCheckStartNodeForm, useWorkflowRun } from '../hooks' +import { useWorkflowRun } from '../hooks' import type { StartNodeType } from '../nodes/start/types' import { TransferMethod } from '../../base/text-generation/types' import Button from '@/app/components/base/button' @@ -56,8 +56,6 @@ const InputsPanel = ({ onRun }: Props) => { return data }, [fileSettings?.image?.enabled, startVariables]) - const { getProcessedInputs } = useCheckStartNodeForm() - const handleValueChange = (variable: string, v: any) => { const { inputs, @@ -78,8 +76,8 @@ const InputsPanel = ({ onRun }: Props) => { const doRun = useCallback(() => { onRun() - handleRun({ inputs: getProcessedInputs(inputs), files }) - }, [files, getProcessedInputs, handleRun, inputs, onRun]) + handleRun({ inputs, files }) + }, [files, handleRun, inputs, onRun]) const canRun = useMemo(() => { if (files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id)) diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 4c2f55f110..e51594e6ee 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -368,6 +368,7 @@ export type UploadFileSetting = { allowed_file_types: SupportUploadFileTypes[] allowed_file_extensions?: string[] max_length: number + number_limits?: number } export type VisionSetting = { diff --git a/web/types/app.ts b/web/types/app.ts index 0bb6ee184f..7be1d30b85 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -6,6 +6,7 @@ import type { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' +import type { UploadFileSetting } from '@/app/components/workflow/types' export enum Theme { light = 'light', @@ -242,7 +243,7 @@ export type ModelConfig = { dataset_configs: DatasetConfigs file_upload?: { image: VisionSettings - } + } & UploadFileSetting files?: VisionFile[] created_at?: number updated_at?: number