From 1cf76ee7df0cee0d1e9f8d339732f478045dc228 Mon Sep 17 00:00:00 2001 From: heheer Date: Thu, 26 Sep 2024 13:48:03 +0800 Subject: [PATCH] feat: add form input node (#2773) * add node * dispatch * extract InputTypeConfig component * question tip * fix build * fix * fix --- packages/global/core/ai/type.d.ts | 3 +- packages/global/core/chat/type.d.ts | 2 +- packages/global/core/workflow/constants.ts | 10 +- .../global/core/workflow/node/constant.ts | 3 +- .../global/core/workflow/runtime/type.d.ts | 3 + .../global/core/workflow/runtime/utils.ts | 5 + .../core/workflow/template/constants.ts | 6 +- .../template/system/interactive/formInput.ts | 55 +++ .../template/system/interactive/type.d.ts | 56 +++ .../index.ts => interactive/userSelect.ts} | 0 .../template/system/userSelect/type.d.ts | 24 -- packages/service/core/chat/saveChat.ts | 41 +- .../service/core/workflow/dispatch/index.ts | 9 +- .../dispatch/interactive/formInput.ts | 63 +++ .../dispatch/interactive/userSelect.ts | 2 +- .../web/components/common/Icon/constants.ts | 1 + .../core/workflow/template/formInput.svg | 10 + packages/web/i18n/en/app.json | 6 + packages/web/i18n/en/common.json | 22 +- packages/web/i18n/zh/app.json | 6 + packages/web/i18n/zh/common.json | 12 +- .../core/chat/ChatContainer/ChatBox/utils.ts | 55 ++- .../components/renderPluginInput.tsx | 1 - .../core/chat/ChatContainer/type.d.ts | 2 +- .../core/chat/components/AIResponseBox.tsx | 143 ++++++- .../chat/components/WholeResponseModal.tsx | 6 + .../app/src/pages/api/v1/chat/completions.ts | 4 +- .../WorkflowComponents/Flow/index.tsx | 3 +- .../NodeFormInput/InputFormEditModal.tsx | 201 +++++++++ .../Flow/nodes/NodeFormInput/index.tsx | 224 ++++++++++ .../nodes/NodePluginIO/InputEditModal.tsx | 365 ++-------------- .../nodes/NodePluginIO/InputTypeConfig.tsx | 398 ++++++++++++++++++ .../Flow/nodes/NodePluginIO/PluginInput.tsx | 2 +- .../Flow/nodes/NodeUserSelect.tsx | 2 +- 34 files changed, 1326 insertions(+), 419 deletions(-) create mode 100644 packages/global/core/workflow/template/system/interactive/formInput.ts create mode 100644 packages/global/core/workflow/template/system/interactive/type.d.ts rename packages/global/core/workflow/template/system/{userSelect/index.ts => interactive/userSelect.ts} (100%) delete mode 100644 packages/global/core/workflow/template/system/userSelect/type.d.ts create mode 100644 packages/service/core/workflow/dispatch/interactive/formInput.ts create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/formInput.svg create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeFormInput/InputFormEditModal.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeFormInput/index.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index 1522a1a8c..d325b3aaa 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -9,8 +9,7 @@ import type { ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam } from 'openai/resources'; import { ChatMessageTypeEnum } from './constants'; -import { InteractiveNodeResponseItemType } from '../workflow/template/system/userSelect/type'; - +import { InteractiveNodeResponseItemType } from '../workflow/template/system/interactive/type'; export * from 'openai/resources'; // Extension of ChatCompletionMessageParam, Add file url type diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index 67b1dadc9..0cdcc1db2 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -15,7 +15,7 @@ import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d'; import { DatasetSearchModeEnum } from '../dataset/constants'; import { DispatchNodeResponseType } from '../workflow/runtime/type.d'; import { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type'; -import { InteractiveNodeResponseItemType } from '../workflow/template/system/userSelect/type'; +import { InteractiveNodeResponseItemType } from '../workflow/template/system/interactive/type'; export type ChatSchema = { _id: string; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 0b2b76242..9e7534fad 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -148,7 +148,10 @@ export enum NodeInputKeyEnum { // loop start loopStartInput = 'loopStartInput', // loop end - loopEndInput = 'loopEndInput' + loopEndInput = 'loopEndInput', + + // form input + userInputForms = 'userInputForms' } export enum NodeOutputKeyEnum { @@ -197,7 +200,10 @@ export enum NodeOutputKeyEnum { loopArray = 'loopArray', // loop start - loopStartInput = 'loopStartInput' + loopStartInput = 'loopStartInput', + + // form input + formInputResult = 'formInputResult' } export enum VariableInputEnum { diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index 34e2a6f12..dd2872ee4 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -128,7 +128,8 @@ export enum FlowNodeTypeEnum { userSelect = 'userSelect', loop = 'loop', loopStart = 'loopStart', - loopEnd = 'loopEnd' + loopEnd = 'loopEnd', + formInput = 'formInput' } // node IO value type diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 476112a09..43f643727 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -182,6 +182,9 @@ export type DispatchNodeResponseType = { loopInputValue?: any; // loop end loopOutputValue?: any; + + // form input + formInputResult?: string; }; export type DispatchNodeResultType = { diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index f8779207f..83c7859ca 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -54,6 +54,11 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => { ) { return lastValue.interactive; } + + // Check is user input + if (lastValue.interactive.type === 'userInput' && !lastValue.interactive.params.submitted) { + return lastValue.interactive; + } } return null; diff --git a/packages/global/core/workflow/template/constants.ts b/packages/global/core/workflow/template/constants.ts index f4da162f1..ba2d94edc 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -28,10 +28,11 @@ import { CodeNode } from './system/sandbox'; import { TextEditorNode } from './system/textEditor'; import { CustomFeedbackNode } from './system/customFeedback'; import { ReadFilesNode } from './system/readFiles'; -import { UserSelectNode } from './system/userSelect/index'; +import { UserSelectNode } from './system/interactive/userSelect'; import { LoopNode } from './system/loop/loop'; import { LoopStartNode } from './system/loop/loopStart'; import { LoopEndNode } from './system/loop/loopEnd'; +import { FormInputNode } from './system/interactive/formInput'; const systemNodes: FlowNodeTemplateType[] = [ AiChatModule, @@ -58,7 +59,8 @@ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [ WorkflowStart, ...systemNodes, CustomFeedbackNode, - UserSelectNode + UserSelectNode, + FormInputNode ]; /* plugin flow module templates */ export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [ diff --git a/packages/global/core/workflow/template/system/interactive/formInput.ts b/packages/global/core/workflow/template/system/interactive/formInput.ts new file mode 100644 index 000000000..5c2a60bf8 --- /dev/null +++ b/packages/global/core/workflow/template/system/interactive/formInput.ts @@ -0,0 +1,55 @@ +import { i18nT } from '../../../../../../web/i18n/utils'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + FlowNodeTypeEnum +} from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node'; +import { getHandleConfig } from '../../utils'; + +export const FormInputNode: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.formInput, + templateType: FlowNodeTemplateTypeEnum.interactive, + flowNodeType: FlowNodeTypeEnum.formInput, + sourceHandle: getHandleConfig(true, true, true, true), + targetHandle: getHandleConfig(true, true, true, true), + avatar: 'core/workflow/template/formInput', + name: i18nT('app:workflow.form_input'), + intro: i18nT(`app:workflow.form_input_tip`), + showStatus: true, + version: '4811', + inputs: [ + { + key: NodeInputKeyEnum.description, + renderTypeList: [FlowNodeInputTypeEnum.textarea], + valueType: WorkflowIOValueTypeEnum.string, + label: i18nT('app:workflow.select_description'), + description: i18nT('app:workflow.input_description_tip'), + placeholder: i18nT('app:workflow.form_input_description_placeholder') + }, + { + key: NodeInputKeyEnum.userInputForms, + renderTypeList: [FlowNodeInputTypeEnum.custom], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + value: [] + } + ], + outputs: [ + { + id: NodeOutputKeyEnum.formInputResult, + key: NodeOutputKeyEnum.formInputResult, + required: true, + label: i18nT('app:workflow.form_input_result'), + description: i18nT('app:workflow.form_input_result_tip'), + valueType: WorkflowIOValueTypeEnum.object, + type: FlowNodeOutputTypeEnum.static + } + ] +}; diff --git a/packages/global/core/workflow/template/system/interactive/type.d.ts b/packages/global/core/workflow/template/system/interactive/type.d.ts new file mode 100644 index 000000000..f074dbf82 --- /dev/null +++ b/packages/global/core/workflow/template/system/interactive/type.d.ts @@ -0,0 +1,56 @@ +import { NodeOutputItemType } from '../../../../chat/type'; +import { FlowNodeOutputItemType } from '../../../type/io'; +import { RuntimeEdgeItemType } from '../../../runtime/type'; +import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; +import { WorkflowIOValueTypeEnum } from 'core/workflow/constants'; + +export type UserSelectOptionItemType = { + key: string; + value: string; +}; + +export type UserInputFormItemType = { + type: FlowNodeInputTypeEnum; + key: string; + label: string; + value: any; + valueType: WorkflowIOValueTypeEnum; + description?: string; + defaultValue?: any; + required: boolean; + + // input & textarea + maxLength?: number; + // numberInput + max?: number; + min?: number; + // select + list?: { label: string; value: string }[]; +}; + +type InteractiveBasicType = { + entryNodeIds: string[]; + memoryEdges: RuntimeEdgeItemType[]; + nodeOutputs: NodeOutputItemType[]; +}; + +type UserSelectInteractive = { + type: 'userSelect'; + params: { + description: string; + userSelectOptions: UserSelectOptionItemType[]; + userSelectedVal?: string; + }; +}; + +type UserInputInteractive = { + type: 'userInput'; + params: { + description: string; + inputForm: UserInputFormItemType[]; + submitted?: boolean; + }; +}; + +export type InteractiveNodeResponseItemType = InteractiveBasicType & + (UserSelectInteractive | UserInputInteractive); diff --git a/packages/global/core/workflow/template/system/userSelect/index.ts b/packages/global/core/workflow/template/system/interactive/userSelect.ts similarity index 100% rename from packages/global/core/workflow/template/system/userSelect/index.ts rename to packages/global/core/workflow/template/system/interactive/userSelect.ts diff --git a/packages/global/core/workflow/template/system/userSelect/type.d.ts b/packages/global/core/workflow/template/system/userSelect/type.d.ts deleted file mode 100644 index 50e977a6e..000000000 --- a/packages/global/core/workflow/template/system/userSelect/type.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NodeOutputItemType } from '../../../../chat/type'; -import { FlowNodeOutputItemType } from '../../../type/io'; -import { RuntimeEdgeItemType } from '../../../runtime/type'; - -export type UserSelectOptionItemType = { - key: string; - value: string; -}; - -type InteractiveBasicType = { - entryNodeIds: string[]; - memoryEdges: RuntimeEdgeItemType[]; - nodeOutputs: NodeOutputItemType[]; -}; -type UserSelectInteractive = { - type: 'userSelect'; - params: { - description: string; - userSelectOptions: UserSelectOptionItemType[]; - userSelectedVal?: string; - }; -}; - -export type InteractiveNodeResponseItemType = InteractiveBasicType & UserSelectInteractive; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 97f2bcdb6..b4b046f79 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -121,7 +121,7 @@ export const updateInteractiveChat = async ({ appId, teamId, tmbId, - userSelectedVal, + userInteractiveVal, aiResponse, newVariables, newTitle @@ -130,7 +130,7 @@ export const updateInteractiveChat = async ({ appId: string; teamId: string; tmbId: string; - userSelectedVal: string; + userInteractiveVal: string; aiResponse: AIChatItemType & { dataId?: string }; newVariables?: Record; newTitle: string; @@ -153,13 +153,38 @@ export const updateInteractiveChat = async ({ return; } - interactiveValue.interactive = { - ...interactiveValue.interactive, - params: { - ...interactiveValue.interactive.params, - userSelectedVal + const parsedUserInteractiveVal = (() => { + try { + return JSON.parse(userInteractiveVal); + } catch (err) { + return userInteractiveVal; } - }; + })(); + interactiveValue.interactive = + interactiveValue.interactive.type === 'userSelect' + ? { + ...interactiveValue.interactive, + params: { + ...interactiveValue.interactive.params, + userSelectedVal: userInteractiveVal + } + } + : { + ...interactiveValue.interactive, + params: { + ...interactiveValue.interactive.params, + inputForm: interactiveValue.interactive.params.inputForm.map((item) => { + const itemValue = parsedUserInteractiveVal[item.label]; + return itemValue !== undefined + ? { + ...item, + value: itemValue + } + : item; + }), + submitted: true + } + }; if (aiResponse.customFeedbacks) { chatItem.customFeedbacks = chatItem.customFeedbacks diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 6f92e49a1..344602ba5 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -64,11 +64,12 @@ import { dispatchUserSelect } from './interactive/userSelect'; import { InteractiveNodeResponseItemType, UserSelectInteractive -} from '@fastgpt/global/core/workflow/template/system/userSelect/type'; +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { dispatchRunAppNode } from './plugin/runApp'; import { dispatchLoop } from './loop/runLoop'; import { dispatchLoopEnd } from './loop/runLoopEnd'; import { dispatchLoopStart } from './loop/runLoopStart'; +import { dispatchFormInput } from './interactive/formInput'; const callbackMap: Record = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -97,6 +98,7 @@ const callbackMap: Record = { [FlowNodeTypeEnum.loop]: dispatchLoop, [FlowNodeTypeEnum.loopStart]: dispatchLoopStart, [FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd, + [FlowNodeTypeEnum.formInput]: dispatchFormInput, // none [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, @@ -584,7 +586,10 @@ export async function dispatchWorkFlow(data: Props): Promise { // Interactive node is not the entry node, return interactive result - if (item.flowNodeType !== FlowNodeTypeEnum.userSelect) { + if ( + item.flowNodeType !== FlowNodeTypeEnum.userSelect && + item.flowNodeType !== FlowNodeTypeEnum.formInput + ) { item.isEntry = false; } }); diff --git a/packages/service/core/workflow/dispatch/interactive/formInput.ts b/packages/service/core/workflow/dispatch/interactive/formInput.ts new file mode 100644 index 000000000..532fc67bb --- /dev/null +++ b/packages/service/core/workflow/dispatch/interactive/formInput.ts @@ -0,0 +1,63 @@ +import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { + DispatchNodeResultType, + ModuleDispatchProps +} from '@fastgpt/global/core/workflow/runtime/type'; +import { + UserInputFormItemType, + UserInputInteractive +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.description]: string; + [NodeInputKeyEnum.userInputForms]: UserInputFormItemType[]; +}>; +type FormInputResponse = DispatchNodeResultType<{ + [DispatchNodeResponseKeyEnum.interactive]?: UserInputInteractive; + [NodeOutputKeyEnum.formInputResult]?: Record; +}>; + +export const dispatchFormInput = async (props: Props): Promise => { + const { + histories, + node, + params: { description, userInputForms }, + query + } = props; + const { isEntry } = node; + + // Interactive node is not the entry node, return interactive result + if (!isEntry) { + return { + [DispatchNodeResponseKeyEnum.interactive]: { + type: 'userInput', + params: { + description, + inputForm: userInputForms + } + } + }; + } + + node.isEntry = false; + + const { text } = chatValue2RuntimePrompt(query); + const userInputVal = (() => { + try { + return JSON.parse(text); + } catch (error) { + return text; + } + })(); + + return { + [DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2), // Removes the current session record as the history of subsequent nodes + ...userInputVal, + [NodeOutputKeyEnum.formInputResult]: userInputVal, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + formInputResult: userInputVal + } + }; +}; diff --git a/packages/service/core/workflow/dispatch/interactive/userSelect.ts b/packages/service/core/workflow/dispatch/interactive/userSelect.ts index 5157bec1b..090ebf9f3 100644 --- a/packages/service/core/workflow/dispatch/interactive/userSelect.ts +++ b/packages/service/core/workflow/dispatch/interactive/userSelect.ts @@ -8,7 +8,7 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils'; import type { UserSelectInteractive, UserSelectOptionItemType -} from '@fastgpt/global/core/workflow/template/system/userSelect/type'; +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; type Props = ModuleDispatchProps<{ diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index a4dc2f20a..6f82a1a6b 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -223,6 +223,7 @@ export const iconPaths = { 'core/workflow/template/extractJson': () => import('./icons/core/workflow/template/extractJson.svg'), 'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'), + 'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'), 'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'), 'core/workflow/template/httpRequest': () => import('./icons/core/workflow/template/httpRequest.svg'), diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/formInput.svg b/packages/web/components/common/Icon/icons/core/workflow/template/formInput.svg new file mode 100644 index 000000000..c7f4156bf --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/formInput.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index c47186cc4..47dfe35f8 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -142,6 +142,12 @@ "week.Wednesday": "Wednesday", "workflow.Input guide": "Input Guide", "workflow.file_url": "Document Link", + "workflow.form_input": "Form input", + "workflow.form_input_description_placeholder": "For example: \nAdd your information", + "workflow.form_input_result": "Full input result", + "workflow.form_input_result_tip": "An object of full result", + "workflow.form_input_tip": " This module can configure multiple inputs to guide users in entering specific content.", + "workflow.input_description_tip": "You can add a description to explain to users what they need to input", "workflow.read_files": "Document Parsing", "workflow.read_files_result": "Document Parsing Result", "workflow.read_files_result_desc": "Original document text, consisting of file names and document content, separated by hyphens between multiple files.", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index f70d57739..19f551a57 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -24,6 +24,7 @@ "Rename": "Rename", "Resume": "Resume", "Running": "Running", + "Submit": "Submit", "UnKnow": "Unknown", "Warning": "Warning", "add_new": "Add New", @@ -82,6 +83,7 @@ "common.Add": "Add", "common.Add New": "Add New", "common.Add Success": "Added Successfully", + "common.Add_new_input": "Add new input", "common.All": "All", "common.Cancel": "Cancel", "common.Choose": "Choose", @@ -93,6 +95,7 @@ "common.Confirm Move": "Move Here", "common.Confirm Update": "Confirm Update", "common.Confirm to leave the page": "Confirm to Leave This Page?", + "common.Continue_Adding": "Continue adding", "common.Copy": "Copy", "common.Copy Successful": "Copied Successfully", "common.Copy_failed": "Copy Failed, Please Copy Manually", @@ -426,6 +429,11 @@ "core.chat.response.Read complete response tips": "Click to View Detailed Process", "core.chat.response.Tool call tokens": "Tool Call Tokens Consumption", "core.chat.response.context total length": "Total Context Length", + "core.chat.response.form_input_result": "Form input result", + "core.chat.response.loop_input": "Loop Input Array", + "core.chat.response.loop_input_element": "Loop Input Element", + "core.chat.response.loop_output": "Loop Output Array", + "core.chat.response.loop_output_element": "Loop Output Element", "core.chat.response.module cq": "Question Classification List", "core.chat.response.module cq result": "Classification Result", "core.chat.response.module extract description": "Extract Background Description", @@ -448,10 +456,6 @@ "core.chat.response.text output": "Text Output", "core.chat.response.update_var_result": "Variable Update Result (Displays Multiple Variable Update Results in Order)", "core.chat.response.user_select_result": "User Selection Result", - "core.chat.response.loop_input": "Loop Input Array", - "core.chat.response.loop_output": "Loop Output Array", - "core.chat.response.loop_input_element": "Loop Input Element", - "core.chat.response.loop_output_element": "Loop Output Element", "core.chat.retry": "Regenerate", "core.chat.tts.Stop Speech": "Stop", "core.common.tip.leave page": "Content has been modified, confirm to leave the page?", @@ -701,6 +705,10 @@ "core.module.input.label.chat history": "Chat History", "core.module.input.label.user question": "User Question", "core.module.input.placeholder.Classify background": "For example:\n1. AIGC (Artificial Intelligence Generated Content) refers to the use of artificial intelligence technology to automatically or semi-automatically generate digital content, such as text, images, music, videos, etc.\n2. AIGC technology includes but is not limited to natural language processing, computer vision, machine learning, and deep learning. These technologies can create new content or modify existing content to meet specific creative, educational, entertainment, or informational needs.", + "core.module.input_description": "Description", + "core.module.input_form": "Input form", + "core.module.input_name": "Input name", + "core.module.input_type": "Input type", "core.module.laf.Select laf function": "Select LAF Function", "core.module.output.description.Ai response content": "Will be triggered after the stream reply is completed", "core.module.output.description.New context": "Splice the current reply content with the history records and return it as the new context", @@ -729,6 +737,7 @@ "core.module.template.system_config_info": "Can configure application system parameters", "core.module.template.work_start": "Process starts", "core.module.templates.Load plugin error": "Failed to Load Plugin", + "core.module.variable add option": "Add Option", "core.module.variable.Custom type": "Custom Variable", "core.module.variable.add option": "Add Option", "core.module.variable.input type": "Text", @@ -743,7 +752,6 @@ "core.module.variable.variable option is required": "Options Cannot Be All Empty", "core.module.variable.variable option is value is required": "Option Content Cannot Be Empty", "core.module.variable.variable options": "Options", - "core.module.variable add option": "Add Option", "core.plugin.Custom headers": "Custom Request Headers", "core.plugin.Free": "This plugin does not consume points", "core.plugin.Get Plugin Module Detail Failed": "Failed to Retrieve Plugin Information", @@ -841,12 +849,12 @@ "dataset.deleteFolderTips": "Confirm to Delete This Folder and All Its Contained Datasets? Data Cannot Be Recovered After Deletion, Please Confirm!", "dataset.test.noResult": "No Search Results", "error.Create failed": "Create failed", + "error.code_error": "Verification code error", "error.fileNotFound": "File not found~", "error.inheritPermissionError": "Inherit permission Error", "error.missingParams": "Insufficient parameters", "error.upload_file_error_filename": "{{name}} Upload Failed", "error.username_empty": "Account cannot be empty", - "error.code_error": "Verification code error", "extraction_results": "Extraction Results", "field_name": "Field Name", "free": "Free", @@ -1187,4 +1195,4 @@ "verification": "Verification", "xx_search_result": "{{key}} Search Results", "yes": "Yes" -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 5405b0b53..76c88401d 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -143,6 +143,12 @@ "week.Wednesday": "星期三", "workflow.Input guide": "填写说明", "workflow.file_url": "文档链接", + "workflow.form_input": "表单输入", + "workflow.form_input_description_placeholder": "例如:\n补充您的信息", + "workflow.form_input_result": "完整输入结果", + "workflow.form_input_result_tip": "一个包含完整结果的对象", + "workflow.form_input_tip": "该模块可以配置多种输入,引导用户输入特定内容。", + "workflow.input_description_tip": "你可以添加一段说明文字,用以向用户说明需要输入的内容", "workflow.read_files": "文档解析", "workflow.read_files_result": "文档解析结果", "workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 24d85239e..0a050994e 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -24,6 +24,7 @@ "Rename": "重命名", "Resume": "恢复", "Running": "运行中", + "Submit": "提交", "UnKnow": "未知", "Warning": "提示", "add_new": "新增", @@ -82,6 +83,7 @@ "common.Add": "添加", "common.Add New": "新增", "common.Add Success": "添加成功", + "common.Add_new_input": "新增输入", "common.All": "全部", "common.Cancel": "取消", "common.Choose": "选择", @@ -93,6 +95,7 @@ "common.Confirm Move": "移动到这", "common.Confirm Update": "确认更新", "common.Confirm to leave the page": "确认离开该页面?", + "common.Continue_Adding": "继续添加", "common.Copy": "复制", "common.Copy Successful": "复制成功", "common.Copy_failed": "复制失败,请手动复制", @@ -426,6 +429,7 @@ "core.chat.response.Read complete response tips": "点击查看详细流程", "core.chat.response.Tool call tokens": "工具调用 tokens 消耗", "core.chat.response.context total length": "上下文总长度", + "core.chat.response.form_input_result": "表单输入结果", "core.chat.response.loop_input": "输入数组", "core.chat.response.loop_input_element": "输入数组元素", "core.chat.response.loop_output": "输出数组", @@ -701,6 +705,10 @@ "core.module.input.label.chat history": "聊天记录", "core.module.input.label.user question": "用户问题", "core.module.input.placeholder.Classify background": "例如:\n1. AIGC(人工智能生成内容)是指使用人工智能技术自动或半自动地生成数字内容,如文本、图像、音乐、视频等。\n2. AIGC 技术包括但不限于自然语言处理、计算机视觉、机器学习和深度学习。这些技术可以创建新内容或修改现有内容,以满足特定的创意、教育、娱乐或信息需求。", + "core.module.input_description": "输入描述", + "core.module.input_form": "输入字段", + "core.module.input_name": "输入名", + "core.module.input_type": "输入类型", "core.module.laf.Select laf function": "选择 laf 函数", "core.module.output.description.Ai response content": "将在 stream 回复完毕后触发", "core.module.output.description.New context": "将本次回复内容拼接上历史记录,作为新的上下文返回", @@ -730,6 +738,7 @@ "core.module.template.system_config_info": "可以配置应用的系统参数", "core.module.template.work_start": "流程开始", "core.module.templates.Load plugin error": "加载插件失败", + "core.module.variable add option": "添加选项", "core.module.variable.Custom type": "自定义变量", "core.module.variable.add option": "添加选项", "core.module.variable.input type": "文本", @@ -744,7 +753,6 @@ "core.module.variable.variable option is required": "选项不能全空", "core.module.variable.variable option is value is required": "选项内容不能为空", "core.module.variable.variable options": "选项", - "core.module.variable add option": "添加选项", "core.plugin.Custom headers": "自定义请求头", "core.plugin.Free": "该插件无需积分消耗~", "core.plugin.Get Plugin Module Detail Failed": "加载插件异常", @@ -842,12 +850,12 @@ "dataset.deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!", "dataset.test.noResult": "搜索结果为空", "error.Create failed": "创建失败", + "error.code_error": "验证码错误", "error.fileNotFound": "文件找不到了~", "error.inheritPermissionError": "权限继承错误", "error.missingParams": "参数缺失", "error.upload_file_error_filename": "{{name}} 上传失败", "error.username_empty": "账号不能为空", - "error.code_error": "验证码错误", "extraction_results": "提取结果", "field_name": "字段名", "free": "免费", diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts index a1f98496a..cb1d10e43 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts @@ -50,18 +50,26 @@ export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[]) lastAIHistory.value.length - 1 ] as AIChatItemValueItemType; - return ( + if ( lastMessageValue && lastMessageValue.type === ChatItemValueTypeEnum.interactive && - !!lastMessageValue?.interactive?.params && + !!lastMessageValue?.interactive?.params + ) { + const params = lastMessageValue.interactive.params; // 如果用户选择了,则不认为是交互模式(可能是上一轮以交互结尾,发起的新的一轮对话) - !lastMessageValue?.interactive?.params?.userSelectedVal - ); + if ('userSelectOptions' in params && 'userSelectedVal' in params) { + return !params.userSelectedVal; + } else if ('inputForm' in params && 'submitted' in params) { + return !params.submitted; + } + } + + return false; }; export const setUserSelectResultToHistories = ( histories: ChatSiteItemType[], - selectVal: string + interactiveVal: string ): ChatSiteItemType[] => { if (histories.length === 0) return histories; @@ -77,18 +85,33 @@ export const setUserSelectResultToHistories = ( ) return val; - return { - ...val, - interactive: { - ...val.interactive, - params: { - ...val.interactive.params, - userSelectedVal: val.interactive.params.userSelectOptions.find( - (item) => item.value === selectVal - )?.value + if (val.interactive.type === 'userSelect') { + return { + ...val, + interactive: { + ...val.interactive, + params: { + ...val.interactive.params, + userSelectedVal: val.interactive.params.userSelectOptions.find( + (item) => item.value === interactiveVal + )?.value + } } - } - }; + }; + } + + if (val.interactive.type === 'userInput') { + return { + ...val, + interactive: { + ...val.interactive, + params: { + ...val.interactive.params, + submitted: true + } + } + }; + } }); return { diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx index b3a34b682..221135782 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput.tsx @@ -6,7 +6,6 @@ import { NumberInput, NumberInputField, NumberInputStepper, - Select, Switch, Textarea } from '@chakra-ui/react'; diff --git a/projects/app/src/components/core/chat/ChatContainer/type.d.ts b/projects/app/src/components/core/chat/ChatContainer/type.d.ts index 3e02e071d..2248b932a 100644 --- a/projects/app/src/components/core/chat/ChatContainer/type.d.ts +++ b/projects/app/src/components/core/chat/ChatContainer/type.d.ts @@ -1,7 +1,7 @@ import { StreamResponseType } from '@/web/common/api/fetch'; import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type'; -import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type'; +import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; export type generatingMessageProps = { event: SseResponseEventEnum; diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 9efa62e9d..a7eca5d95 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -7,7 +7,14 @@ import { AccordionPanel, Box, Button, - Flex + Flex, + Input, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, + Textarea } from '@chakra-ui/react'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { @@ -15,12 +22,22 @@ import { ToolModuleResponseItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type'; +import { + InteractiveBasicType, + UserInputInteractive, + UserSelectInteractive +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { isEqual } from 'lodash'; import { onSendPrompt } from '../ChatContainer/useChat'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { useTranslation } from 'react-i18next'; +import { Controller, useForm } from 'react-hook-form'; +import MySelect from '@fastgpt/web/components/common/MySelect'; type props = { value: UserChatItemValueItemType | AIChatItemValueItemType; @@ -123,10 +140,10 @@ ${toolResponse}`} }, (prevProps, nextProps) => isEqual(prevProps, nextProps) ); -const RenderInteractive = React.memo(function RenderInteractive({ +const RenderUserSelectInteractive = React.memo(function RenderInteractive({ interactive }: { - interactive: InteractiveNodeResponseItemType; + interactive: InteractiveBasicType & UserSelectInteractive; }) { return ( <> @@ -166,6 +183,114 @@ const RenderInteractive = React.memo(function RenderInteractive({ ); }); +const RenderUserFormInteractive = React.memo(function RenderFormInput({ + interactive +}: { + interactive: InteractiveBasicType & UserInputInteractive; +}) { + const { t } = useTranslation(); + const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm(); + + const onSubmit = useCallback((data: any) => { + onSendPrompt({ + text: JSON.stringify(data), + isInteractivePrompt: true + }); + }, []); + + useEffect(() => { + if (interactive.type === 'userInput') { + const defaultValues = interactive.params.inputForm?.reduce( + (acc: Record, item) => { + acc[item.label] = !!item.value ? item.value : item.defaultValue; + return acc; + }, + {} + ); + reset(defaultValues); + } + }, []); + + return ( + + {interactive.params.inputForm?.map((input) => ( + + + {input.label} + + + {input.type === FlowNodeInputTypeEnum.input && ( + + )} + {input.type === FlowNodeInputTypeEnum.textarea && ( +