From 2bdda4638d1a368cf0ff28a7f5a565519d4bc569 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Sun, 15 Sep 2024 22:41:05 +0800 Subject: [PATCH] Feat: Workflow loop node;feat: support openai o1;perf: query extension prompt;fix: intro was not delivered when the datase was created (#2719) * feat: loop node (#2675) * loop node frontend * loop-node * fix-code * fix version * fix * fix * fix * perf: loop array code * perf: get histories error tip * feat: support openai o1 * perf: query extension prompt * feat: 4811 doc * remove log * fix: loop node zindex & variable picker type (#2710) * perf: performance * perf: workflow performance * remove uninvalid code * perf:code * fix: invoice table refresh * perf: loop node data type * fix: loop node store assistants * perf: target connection * feat: loop node support help line * perf: add default icon --------- Co-authored-by: heheer --- .../zh-cn/docs/development/upgrading/4811.md | 79 +++++- packages/global/core/chat/adapt.ts | 2 +- packages/global/core/workflow/constants.ts | 21 +- .../global/core/workflow/node/constant.ts | 9 +- .../global/core/workflow/runtime/type.d.ts | 9 + .../core/workflow/template/constants.ts | 10 +- .../global/core/workflow/template/input.ts | 22 ++ .../workflow/template/system/loop/loop.ts | 54 ++++ .../workflow/template/system/loop/loopEnd.ts | 34 +++ .../template/system/loop/loopStart.ts | 34 +++ packages/global/core/workflow/type/node.d.ts | 1 + packages/global/package.json | 2 +- .../core/ai/functions/queryExtension.ts | 38 ++- .../workflow/dispatch/abandoned/runApp.ts | 1 + .../dispatch/agent/classifyQuestion.ts | 3 +- .../core/workflow/dispatch/agent/extract.ts | 3 +- .../workflow/dispatch/agent/runTool/index.ts | 5 +- .../dispatch/agent/runTool/promptCall.ts | 22 +- .../dispatch/agent/runTool/toolChoice.ts | 41 ++- .../core/workflow/dispatch/chat/oneapi.ts | 26 +- .../service/core/workflow/dispatch/index.ts | 12 +- .../core/workflow/dispatch/loop/runLoop.ts | 93 +++++++ .../core/workflow/dispatch/loop/runLoopEnd.ts | 21 ++ .../workflow/dispatch/loop/runLoopStart.ts | 23 ++ .../core/workflow/dispatch/plugin/runApp.ts | 1 + .../core/workflow/dispatch/tools/answer.ts | 2 +- .../web/components/common/EmptyTip/index.tsx | 5 +- .../web/components/common/Icon/constants.ts | 4 + .../icons/core/workflow/inputType/array.svg | 5 + .../icons/core/workflow/template/loop.svg | 11 + .../icons/core/workflow/template/loopEnd.svg | 11 + .../core/workflow/template/loopStart.svg | 11 + .../common/MySelect/MultipleRowSelect.tsx | 9 +- .../web/components/common/MySelect/type.d.ts | 1 + .../plugins/VariablePickerPlugin/index.tsx | 2 +- packages/web/i18n/en/common.json | 6 +- packages/web/i18n/en/workflow.json | 4 + packages/web/i18n/zh/common.json | 6 +- packages/web/i18n/zh/workflow.json | 10 + pnpm-lock.yaml | 24 +- projects/app/data/config.json | 54 ++++ .../app/src/components/Markdown/index.tsx | 1 - .../src/components/common/NextHead/index.tsx | 13 +- .../ChatContainer/ChatBox/Input/ChatInput.tsx | 1 - .../core/chat/components/AIResponseBox.tsx | 3 +- .../chat/components/WholeResponseModal.tsx | 27 +- .../account/components/bill/InvoiceTable.tsx | 8 +- .../src/pages/api/core/chat/getHistories.ts | 112 ++++---- .../app/src/pages/api/core/dataset/create.ts | 2 + .../app/src/pages/api/v1/chat/completions.ts | 6 +- .../app/detail/components/Plugin/Header.tsx | 47 ++-- .../detail/components/SimpleApp/EditForm.tsx | 4 +- .../app/detail/components/Workflow/Header.tsx | 50 ++-- .../components/WorkflowComponents/AppCard.tsx | 7 +- .../Flow/ImportSettings.tsx | 1 - .../Flow/NodeTemplatesModal.tsx | 45 ++- .../Flow/components/ButtonEdge.tsx | 13 +- .../Flow/components/Container.tsx | 7 +- .../Flow/components/IOTitle.tsx | 2 +- .../Flow/hooks/useKeyboard.tsx | 9 +- .../Flow/hooks/useWorkflow.tsx | 263 +++++++++++++++--- .../WorkflowComponents/Flow/index.tsx | 9 +- .../Flow/nodes/Loop/NodeLoop.tsx | 88 ++++++ .../Flow/nodes/Loop/NodeLoopEnd.tsx | 93 +++++++ .../Flow/nodes/Loop/NodeLoopStart.tsx | 148 ++++++++++ .../Flow/nodes/NodeAnswer.tsx | 41 +-- .../Flow/nodes/NodeCQNode.tsx | 18 +- .../Flow/nodes/NodeCode.tsx | 64 +++-- .../Flow/nodes/NodeDatasetConcat.tsx | 218 ++++++++------- .../WorkflowComponents/Flow/nodes/NodeLaf.tsx | 146 ++++++---- .../nodes/NodePluginIO/InputEditModal.tsx | 4 +- .../Flow/nodes/NodePluginIO/VariableTable.tsx | 4 +- .../Flow/nodes/NodeSimple.tsx | 68 ++--- .../Flow/nodes/NodeSystemConfig.tsx | 2 +- .../nodes/render/Handle/ConnectionHandle.tsx | 48 +++- .../Flow/nodes/render/Handle/index.tsx | 41 +-- .../Flow/nodes/render/NodeCard.tsx | 49 +++- .../render/RenderInput/FieldEditModal.tsx | 22 +- .../Flow/nodes/render/RenderInput/Label.tsx | 11 +- .../RenderInput/templates/Reference.tsx | 28 +- .../render/RenderOutput/FieldEditModal.tsx | 22 +- .../components/WorkflowComponents/context.tsx | 189 ++++++------- .../components/WorkflowComponents/utils.tsx | 17 +- projects/app/src/service/events/generateQA.ts | 9 +- projects/app/src/web/common/system/utils.ts | 6 +- projects/app/src/web/core/workflow/utils.ts | 24 +- 86 files changed, 2002 insertions(+), 719 deletions(-) create mode 100644 packages/global/core/workflow/template/system/loop/loop.ts create mode 100644 packages/global/core/workflow/template/system/loop/loopEnd.ts create mode 100644 packages/global/core/workflow/template/system/loop/loopStart.ts create mode 100644 packages/service/core/workflow/dispatch/loop/runLoop.ts create mode 100644 packages/service/core/workflow/dispatch/loop/runLoopEnd.ts create mode 100644 packages/service/core/workflow/dispatch/loop/runLoopStart.ts create mode 100644 packages/web/components/common/Icon/icons/core/workflow/inputType/array.svg create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/loop.svg create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopEnd.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx diff --git a/docSite/content/zh-cn/docs/development/upgrading/4811.md b/docSite/content/zh-cn/docs/development/upgrading/4811.md index 600023c2c68..06523f25820 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4811.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4811.md @@ -11,17 +11,84 @@ weight: 813 ### 1. 做好数据备份 +### 2. 修改配置文件 + +如需增加 openai o1 模型,可添加如下配置: + +```json +{ + "model": "o1-mini", + "name": "o1-mini", + "avatar": "/imgs/model/openai.svg", + "maxContext": 125000, + "maxResponse": 4000, + "quoteMaxToken": 120000, + "maxTemperature": 1.2, + "charsPointsPrice": 0, + "censor": false, + "vision": false, + "datasetProcess": false, + "usedInClassify": true, + "usedInExtractFields": true, + "usedInToolCall": true, + "usedInQueryExtension": true, + "toolChoice": false, + "functionCall": false, + "customCQPrompt": "", + "customExtractPrompt": "", + "defaultSystemChatPrompt": "", + "defaultConfig": { + "temperature": 1, + "max_tokens": null, + "stream": false + } +}, +{ + "model": "o1-preview", + "name": "o1-preview", + "avatar": "/imgs/model/openai.svg", + "maxContext": 125000, + "maxResponse": 4000, + "quoteMaxToken": 120000, + "maxTemperature": 1.2, + "charsPointsPrice": 0, + "censor": false, + "vision": false, + "datasetProcess": false, + "usedInClassify": true, + "usedInExtractFields": true, + "usedInToolCall": true, + "usedInQueryExtension": true, + "toolChoice": false, + "functionCall": false, + "customCQPrompt": "", + "customExtractPrompt": "", + "defaultSystemChatPrompt": "", + "defaultConfig": { + "temperature": 1, + "max_tokens": null, + "stream": false + } +} +``` + ------- +### 3. 修改镜像 tag 并重启 + + + ## V4.8.11 更新说明 1. 2. 新增 - 聊天记录滚动加载,不再只加载 30 条。 3. 新增 - 工作流增加触摸板优先模式。 4. 新增 - 沙盒增加字符串转 base64 全局方法。 -5. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。 -5. 优化 - 工作流 handler 性能优化。 -6. 优化 - 工作流快捷键,避免调试测试时也会触发。 -7. 优化 - 流输出,切换 tab 时仍可以继续输出。 -8. 修复 - 知识库选择权限问题。 -9. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。 +5. 新增 - 支持 openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature`、`max_tokens` 和 `stream`配置,o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看。 +6. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。 +7. 优化 - 工作流 handler 性能优化。 +8. 优化 - 工作流快捷键,避免调试测试时也会触发。 +9. 优化 - 流输出,切换 tab 时仍可以继续输出。 +10. 修复 - 知识库选择权限问题。 +11. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。 +12. 修复 - createDataset 接口,intro 为赋值。 diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 472dd290ab6..6eb712876ef 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -344,7 +344,7 @@ export const runtimePrompt2ChatsValue = ( return value; }; -export const getSystemPrompt = (prompt?: string): ChatItemType[] => { +export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemType[] => { if (!prompt) return []; return [ { diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index eab81402362..82e2ee20377 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -24,6 +24,7 @@ export enum WorkflowIOValueTypeEnum { arrayNumber = 'arrayNumber', arrayBoolean = 'arrayBoolean', arrayObject = 'arrayObject', + arrayAny = 'arrayAny', any = 'any', chatHistory = 'chatHistory', @@ -135,7 +136,17 @@ export enum NodeInputKeyEnum { fileUrlList = 'fileUrlList', // user select - userSelectOptions = 'userSelectOptions' + userSelectOptions = 'userSelectOptions', + + // loop + loopInputArray = 'loopInputArray', + childrenNodeIdList = 'childrenNodeIdList', + nodeWidth = 'nodeWidth', + nodeHeight = 'nodeHeight', + // loop start + loopStartInput = 'loopStartInput', + // loop end + loopEndInput = 'loopEndInput' } export enum NodeOutputKeyEnum { @@ -178,7 +189,13 @@ export enum NodeOutputKeyEnum { ifElseResult = 'ifElseResult', //user select - selectResult = 'selectResult' + selectResult = 'selectResult', + + // loop + loopArray = 'loopArray', + + // loop start + loopStartInput = 'loopStartInput' } export enum VariableInputEnum { diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index e34813f1b5e..34e2a6f1217 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -125,7 +125,10 @@ export enum FlowNodeTypeEnum { textEditor = 'textEditor', customFeedback = 'customFeedback', readFiles = 'readFiles', - userSelect = 'userSelect' + userSelect = 'userSelect', + loop = 'loop', + loopStart = 'loopStart', + loopEnd = 'loopEnd' } // node IO value type @@ -162,6 +165,10 @@ export const FlowValueTypeMap = { label: 'array', value: WorkflowIOValueTypeEnum.arrayObject }, + [WorkflowIOValueTypeEnum.arrayAny]: { + label: 'array', + value: WorkflowIOValueTypeEnum.arrayAny + }, [WorkflowIOValueTypeEnum.any]: { label: 'any', value: WorkflowIOValueTypeEnum.any diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 2339fd3ef0c..55f4776a0c5 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -172,6 +172,15 @@ export type DispatchNodeResponseType = { // update var updateVarResult?: any[]; + + // loop + loopResult?: any[]; + loopInput?: any[]; + loopDetail?: ChatHistoryItemResType[]; + // loop start + loopInputValue?: any; + // loop end + loopOutputValue?: any; }; export type DispatchNodeResultType = { diff --git a/packages/global/core/workflow/template/constants.ts b/packages/global/core/workflow/template/constants.ts index 2d2dcb038fc..76439e3b3fd 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -29,6 +29,9 @@ import { TextEditorNode } from './system/textEditor'; import { CustomFeedbackNode } from './system/customFeedback'; import { ReadFilesNodes } from './system/readFiles'; import { UserSelectNode } from './system/userSelect/index'; +import { LoopNode } from './system/loop/loop'; +import { LoopStartNode } from './system/loop/loopStart'; +import { LoopEndNode } from './system/loop/loopEnd'; const systemNodes: FlowNodeTemplateType[] = [ AiChatModule, @@ -46,7 +49,8 @@ const systemNodes: FlowNodeTemplateType[] = [ LafModule, IfElseNode, VariableUpdateNode, - CodeNode + CodeNode, + LoopNode ]; /* app flow module templates */ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [ @@ -74,5 +78,7 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [ EmptyNode, RunPluginModule, RunAppNode, - RunAppModule + RunAppModule, + LoopStartNode, + LoopEndNode ]; diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index a3dce032f8e..cfb7bb39519 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -83,3 +83,25 @@ export const Input_Template_File_Link: FlowNodeInputItemType = { description: i18nT('app:workflow.user_file_input_desc'), valueType: WorkflowIOValueTypeEnum.arrayString }; + +export const Input_Template_Children_Node_List: FlowNodeInputItemType = { + key: NodeInputKeyEnum.childrenNodeIdList, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.arrayString, + label: '', + value: [] +}; +export const Input_Template_Node_Width: FlowNodeInputItemType = { + key: NodeInputKeyEnum.nodeWidth, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: '', + value: 900 +}; +export const Input_Template_Node_Height: FlowNodeInputItemType = { + key: NodeInputKeyEnum.nodeHeight, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: '', + value: 900 +}; diff --git a/packages/global/core/workflow/template/system/loop/loop.ts b/packages/global/core/workflow/template/system/loop/loop.ts new file mode 100644 index 00000000000..b99a1453f48 --- /dev/null +++ b/packages/global/core/workflow/template/system/loop/loop.ts @@ -0,0 +1,54 @@ +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + FlowNodeTypeEnum +} from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { getHandleConfig } from '../../utils'; +import { i18nT } from '../../../../../../web/i18n/utils'; +import { + Input_Template_Children_Node_List, + Input_Template_Node_Height, + Input_Template_Node_Width +} from '../../input'; + +export const LoopNode: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.loop, + templateType: FlowNodeTemplateTypeEnum.tools, + flowNodeType: FlowNodeTypeEnum.loop, + sourceHandle: getHandleConfig(true, true, true, true), + targetHandle: getHandleConfig(true, true, true, true), + avatar: 'core/workflow/template/loop', + name: i18nT('workflow:loop'), + intro: i18nT('workflow:intro_loop'), + showStatus: true, + version: '4811', + inputs: [ + { + key: NodeInputKeyEnum.loopInputArray, + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayAny, + required: true, + label: i18nT('workflow:loop_input_array'), + value: [] + }, + Input_Template_Children_Node_List, + Input_Template_Node_Width, + Input_Template_Node_Height + ], + outputs: [ + { + id: NodeOutputKeyEnum.loopArray, + key: NodeOutputKeyEnum.loopArray, + label: i18nT('workflow:loop_result'), + type: FlowNodeOutputTypeEnum.static, + valueType: WorkflowIOValueTypeEnum.arrayAny + } + ] +}; diff --git a/packages/global/core/workflow/template/system/loop/loopEnd.ts b/packages/global/core/workflow/template/system/loop/loopEnd.ts new file mode 100644 index 00000000000..02633bf7726 --- /dev/null +++ b/packages/global/core/workflow/template/system/loop/loopEnd.ts @@ -0,0 +1,34 @@ +import { i18nT } from '../../../../../../web/i18n/utils'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node'; +import { getHandleConfig } from '../../utils'; + +export const LoopEndNode: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.loopEnd, + templateType: FlowNodeTemplateTypeEnum.systemInput, + flowNodeType: FlowNodeTypeEnum.loopEnd, + sourceHandle: getHandleConfig(false, false, false, false), + targetHandle: getHandleConfig(false, false, false, true), + unique: true, + forbidDelete: true, + avatar: 'core/workflow/template/loopEnd', + name: i18nT('workflow:loop_end'), + showStatus: false, + version: '4811', + inputs: [ + { + key: NodeInputKeyEnum.loopEndInput, + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + required: true, + value: [] + } + ], + outputs: [] +}; diff --git a/packages/global/core/workflow/template/system/loop/loopStart.ts b/packages/global/core/workflow/template/system/loop/loopStart.ts new file mode 100644 index 00000000000..96ded8e69b9 --- /dev/null +++ b/packages/global/core/workflow/template/system/loop/loopStart.ts @@ -0,0 +1,34 @@ +import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node.d'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { getHandleConfig } from '../../utils'; +import { i18nT } from '../../../../../../web/i18n/utils'; + +export const LoopStartNode: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.loopStart, + templateType: FlowNodeTemplateTypeEnum.systemInput, + flowNodeType: FlowNodeTypeEnum.loopStart, + sourceHandle: getHandleConfig(false, true, false, false), + targetHandle: getHandleConfig(false, false, false, false), + avatar: 'core/workflow/template/loopStart', + name: i18nT('workflow:loop_start'), + unique: true, + forbidDelete: true, + showStatus: false, + version: '4811', + inputs: [ + { + key: NodeInputKeyEnum.loopStartInput, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + required: true, + value: '' + } + ], + outputs: [] +}; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 26fdb868194..a5f121418ca 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -95,6 +95,7 @@ export type NodeTemplateListType = { // react flow node type export type FlowNodeItemType = FlowNodeTemplateType & { nodeId: string; + parentNodeId?: string; isError?: boolean; debugResult?: { status: 'running' | 'success' | 'skipped' | 'failed'; diff --git a/packages/global/package.json b/packages/global/package.json index 6852d7777b1..faaab7015b4 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -11,7 +11,7 @@ "jschardet": "3.1.1", "nanoid": "^4.0.1", "next": "14.2.5", - "openai": "4.57.0", + "openai": "4.61.0", "openapi-types": "^12.1.3", "timezones-list": "^3.0.2" }, diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index c316d61ebb5..ad1322fc101 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -4,13 +4,17 @@ import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; +import { getLLMModel } from '../model'; /* query extension - 问题扩展 可以根据上下文,消除指代性问题以及扩展问题,利于检索。 */ -const defaultPrompt = `作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。生成的问题要求指向对象清晰明确,并与“原问题语言相同”。 +const title = global.feConfigs?.systemTitle || 'FastAI'; +const defaultPrompt = `作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。 +生成的问题要求指向对象清晰明确,并与“原问题语言相同”。 + 参考 标中的示例来完成任务。 @@ -49,49 +53,50 @@ A: 护产假的天数根据员工所在的城市而定。请提供您所在的 历史记录: """ Q: 作者是谁? -A: FastGPT 的作者是 labring。 +A: ${title} 的作者是 labring。 """ 原问题: Tell me about him -检索词: ["Introduce labring, the author of FastGPT." ," Background information on author labring." "," Why does labring do FastGPT?"] +检索词: ["Introduce labring, the author of ${title}." ," Background information on author labring." "," Why does labring do ${title}?"] ---------------- 历史记录: """ Q: 对话背景。 -A: 关于 FatGPT 的介绍和使用等问题。 +A: 关于 ${title} 的介绍和使用等问题。 """ 原问题: 你好。 检索词: ["你好"] ---------------- 历史记录: """ -Q: FastGPT 如何收费? -A: FastGPT 收费可以参考…… +Q: ${title} 如何收费? +A: ${title} 收费可以参考…… """ 原问题: 你知道 laf 么? 检索词: ["laf 的官网地址是多少?","laf 的使用教程。","laf 有什么特点和优势。"] ---------------- 历史记录: """ -Q: FastGPT 的优势 +Q: ${title} 的优势 A: 1. 开源 2. 简便 3. 扩展性强 """ 原问题: 介绍下第2点。 -检索词: ["介绍下 FastGPT 简便的优势", "从哪些方面,可以体现出 FastGPT 的简便"]。 +检索词: ["介绍下 ${title} 简便的优势", "从哪些方面,可以体现出 ${title} 的简便"]。 ---------------- 历史记录: """ -Q: 什么是 FastGPT? -A: FastGPT 是一个 RAG 平台。 +Q: 什么是 ${title}? +A: ${title} 是一个 RAG 平台。 Q: 什么是 Laf? A: Laf 是一个云函数开发平台。 """ 原问题: 它们有什么关系? -检索词: ["FastGPT和Laf有什么关系?","介绍下FastGPT","介绍下Laf"] +检索词: ["${title}和Laf有什么关系?","介绍下${title}","介绍下Laf"] ----------------- +----- + 下面是正式的任务: 历史记录: @@ -130,6 +135,8 @@ A: ${chatBg} .join('\n'); const concatFewShot = `${systemFewShot}${historyFewShot}`.trim(); + const modelData = getLLMModel(model); + const ai = getAIApi({ timeout: 480000 }); @@ -144,11 +151,12 @@ A: ${chatBg} } ] as ChatCompletionMessageParam[]; const result = await ai.chat.completions.create({ - model: model, + model: modelData.model, temperature: 0.01, // @ts-ignore messages, - stream: false + stream: false, + ...modelData.defaultConfig }); let answer = result.choices?.[0]?.message?.content || ''; @@ -161,6 +169,8 @@ A: ${chatBg} }; } + // Intercept the content of [] and retain [] + answer = answer.match(/\[.*?\]/)?.[0] || ''; answer = answer.replace(/\\"/g, '"'); try { diff --git a/packages/service/core/workflow/dispatch/abandoned/runApp.ts b/packages/service/core/workflow/dispatch/abandoned/runApp.ts index a8fb32ef825..49693d5b90a 100644 --- a/packages/service/core/workflow/dispatch/abandoned/runApp.ts +++ b/packages/service/core/workflow/dispatch/abandoned/runApp.ts @@ -93,6 +93,7 @@ export const dispatchAppRequest = async (props: Props): Promise => { const { text } = chatValue2RuntimePrompt(assistantResponses); return { + assistantResponses, [DispatchNodeResponseKeyEnum.nodeResponse]: { moduleLogo: appData.avatar, query: userChatInput, diff --git a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts index 94715db1f9f..88956a42854 100644 --- a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts @@ -128,7 +128,8 @@ const completions = async ({ model: cqModel.model, temperature: 0.01, messages: requestMessages, - stream: false + stream: false, + ...cqModel.defaultConfig }); const answer = data.choices?.[0].message?.content || ''; diff --git a/packages/service/core/workflow/dispatch/agent/extract.ts b/packages/service/core/workflow/dispatch/agent/extract.ts index a3754092077..1efd9eb91d7 100644 --- a/packages/service/core/workflow/dispatch/agent/extract.ts +++ b/packages/service/core/workflow/dispatch/agent/extract.ts @@ -355,7 +355,8 @@ Human: ${content}` model: extractModel.model, temperature: 0.01, messages: requestMessages, - stream: false + stream: false, + ...extractModel.defaultConfig }); const answer = data.choices?.[0].message?.content || ''; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/index.ts b/packages/service/core/workflow/dispatch/agent/runTool/index.ts index 44a66391b73..22ca5e75de6 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/index.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/index.ts @@ -14,7 +14,7 @@ import { GPTMessages2Chats, chatValue2RuntimePrompt, chats2GPTMessages, - getSystemPrompt, + getSystemPrompt_ChatItemType, runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils'; @@ -95,7 +95,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< }); const messages: ChatItemType[] = [ - ...getSystemPrompt(systemPrompt), + ...getSystemPrompt_ChatItemType(toolModel.defaultSystemChatPrompt), + ...getSystemPrompt_ChatItemType(systemPrompt), // Add file input prompt to histories ...chatHistories.map((item) => { if (item.obj === ChatRoleEnum.Human) { diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index 74ffb9bd8d2..0fba2b6780f 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -114,15 +114,16 @@ export const runToolWithPromptCall = async ( }) ]); const requestBody = { - ...toolModel?.defaultConfig, model: toolModel.model, temperature: computedTemperature({ model: toolModel, temperature }), + max_completion_tokens: max_tokens, max_tokens, stream, - messages: requestMessages + messages: requestMessages, + ...toolModel?.defaultConfig }; // console.log(JSON.stringify(requestBody, null, 2)); @@ -135,9 +136,13 @@ export const runToolWithPromptCall = async ( Accept: 'application/json, text/plain, */*' } }); + const isStreamResponse = + typeof aiResponse === 'object' && + aiResponse !== null && + ('iterator' in aiResponse || 'controller' in aiResponse); const answer = await (async () => { - if (res && stream) { + if (res && isStreamResponse) { const { answer } = await streamResponse({ res, toolNodes, @@ -164,6 +169,17 @@ export const runToolWithPromptCall = async ( }) }); } + + // 不支持 stream 模式的模型的流失响应 + if (stream && !isStreamResponse) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + text: replaceAnswer + }) + }); + } + // No tool is invoked, indicating that the process is over const gptAssistantResponse: ChatCompletionAssistantMessageParam = { role: ChatCompletionRequestMessageRoleEnum.Assistant, diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index b36cd96a637..b03654759a7 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -128,17 +128,18 @@ export const runToolWithToolChoice = async ( }) ]); const requestBody: any = { - ...toolModel?.defaultConfig, model: toolModel.model, temperature: computedTemperature({ model: toolModel, temperature }), + max_completion_tokens: max_tokens, max_tokens, stream, messages: requestMessages, tools, - tool_choice: 'auto' + tool_choice: 'auto', + ...toolModel?.defaultConfig }; // console.log(JSON.stringify(requestBody, null, 2)); @@ -153,9 +154,13 @@ export const runToolWithToolChoice = async ( Accept: 'application/json, text/plain, */*' } }); + const isStreamResponse = + typeof aiResponse === 'object' && + aiResponse !== null && + ('iterator' in aiResponse || 'controller' in aiResponse); const { answer, toolCalls } = await (async () => { - if (res && stream) { + if (res && isStreamResponse) { return streamResponse({ res, workflowStreamResponse, @@ -165,6 +170,7 @@ export const runToolWithToolChoice = async ( } else { const result = aiResponse as ChatCompletion; const calls = result.choices?.[0]?.message?.tool_calls || []; + const answer = result.choices?.[0]?.message?.content || ''; // 加上name和avatar const toolCalls = calls.map((tool) => { @@ -176,8 +182,33 @@ export const runToolWithToolChoice = async ( }; }); + // 不支持 stream 模式的模型的流失响应 + toolCalls.forEach((tool) => { + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: tool.id, + toolName: tool.toolName, + toolAvatar: tool.toolAvatar, + functionName: tool.function.name, + params: tool.function?.arguments ?? '', + response: '' + } + } + }); + }); + if (answer) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + text: answer + }) + }); + } + return { - answer: result.choices?.[0]?.message?.content || '', + answer, toolCalls: toolCalls }; } @@ -239,7 +270,7 @@ export const runToolWithToolChoice = async ( toolName: '', toolAvatar: '', params: '', - response: sliceStrStartEnd(stringToolResponse, 500, 500) + response: sliceStrStartEnd(stringToolResponse, 2000, 2000) } } }); diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index 763cb7f8217..e2205797899 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -19,7 +19,7 @@ import { countMessagesTokens } from '../../../../common/string/tiktoken/index'; import { chats2GPTMessages, chatValue2RuntimePrompt, - getSystemPrompt, + getSystemPrompt_ChatItemType, GPTMessages2Chats, runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; @@ -153,15 +153,16 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { - if (res && stream) { + if (res && isStreamResponse) { // sse response const { answer } = await streamResponse({ res, @@ -195,6 +201,14 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -91,6 +94,9 @@ const callbackMap: Record = { [FlowNodeTypeEnum.customFeedback]: dispatchCustomFeedback, [FlowNodeTypeEnum.readFiles]: dispatchReadFiles, [FlowNodeTypeEnum.userSelect]: dispatchUserSelect, + [FlowNodeTypeEnum.loop]: dispatchLoop, + [FlowNodeTypeEnum.loopStart]: dispatchLoopStart, + [FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd, // none [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, @@ -160,7 +166,7 @@ export async function dispatchWorkFlow(data: Props): Promise; + [NodeInputKeyEnum.childrenNodeIdList]: string[]; +}>; +type Response = DispatchNodeResultType<{ + [NodeOutputKeyEnum.loopArray]: Array; +}>; + +export const dispatchLoop = async (props: Props): Promise => { + const { + params, + runtimeNodes, + user, + node: { name } + } = props; + const { loopInputArray = [], childrenNodeIdList } = params; + + if (!Array.isArray(loopInputArray)) { + return Promise.reject('Input value is not an array'); + } + if (loopInputArray.length > 50) { + return Promise.reject('Input array length cannot be greater than 50'); + } + + const runNodes = runtimeNodes.filter((node) => childrenNodeIdList.includes(node.nodeId)); + + const outputValueArr = []; + const loopDetail: ChatHistoryItemResType[] = []; + let assistantResponses: AIChatItemValueItemType[] = []; + let totalPoints = 0; + + for await (const item of loopInputArray) { + const response = await dispatchWorkFlow({ + ...props, + runtimeNodes: runNodes.map((node) => + node.flowNodeType === FlowNodeTypeEnum.loopStart + ? { + ...node, + isEntry: true, + inputs: node.inputs.map((input) => + input.key === NodeInputKeyEnum.loopStartInput + ? { + ...input, + value: item + } + : input + ) + } + : { + ...node, + isEntry: false + } + ) + }); + + const loopOutputValue = response.flowResponses.find( + (res) => res.moduleType === FlowNodeTypeEnum.loopEnd + )?.loopOutputValue; + + outputValueArr.push(loopOutputValue); + loopDetail.push(...response.flowResponses); + assistantResponses.push(...response.assistantResponses); + + totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); + } + + return { + [DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + totalPoints: totalPoints, + loopInput: loopInputArray, + loopResult: outputValueArr, + loopDetail: loopDetail + }, + [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ + { + totalPoints: user.openaiAccount?.key ? 0 : totalPoints, + moduleName: name + } + ], + [NodeOutputKeyEnum.loopArray]: outputValueArr + }; +}; diff --git a/packages/service/core/workflow/dispatch/loop/runLoopEnd.ts b/packages/service/core/workflow/dispatch/loop/runLoopEnd.ts new file mode 100644 index 00000000000..9fda4337c75 --- /dev/null +++ b/packages/service/core/workflow/dispatch/loop/runLoopEnd.ts @@ -0,0 +1,21 @@ +import { NodeInputKeyEnum } 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'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.loopEndInput]: any; +}>; +type Response = DispatchNodeResultType<{}>; + +export const dispatchLoopEnd = async (props: Props): Promise => { + const { params } = props; + + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + loopOutputValue: params.loopEndInput + } + }; +}; diff --git a/packages/service/core/workflow/dispatch/loop/runLoopStart.ts b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts new file mode 100644 index 00000000000..c7c4cfb2409 --- /dev/null +++ b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts @@ -0,0 +1,23 @@ +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'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.loopStartInput]: any; +}>; +type Response = DispatchNodeResultType<{ + [NodeOutputKeyEnum.loopStartInput]: any; +}>; + +export const dispatchLoopStart = async (props: Props): Promise => { + const { params } = props; + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + loopInputValue: params.loopStartInput + }, + [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput + }; +}; diff --git a/packages/service/core/workflow/dispatch/plugin/runApp.ts b/packages/service/core/workflow/dispatch/plugin/runApp.ts index b7ba0bafa77..31feb75b4e2 100644 --- a/packages/service/core/workflow/dispatch/plugin/runApp.ts +++ b/packages/service/core/workflow/dispatch/plugin/runApp.ts @@ -107,6 +107,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { const { text } = chatValue2RuntimePrompt(assistantResponses); return { + assistantResponses, [DispatchNodeResponseKeyEnum.runTimes]: runTimes, [DispatchNodeResponseKeyEnum.nodeResponse]: { moduleLogo: appData.avatar, diff --git a/packages/service/core/workflow/dispatch/tools/answer.ts b/packages/service/core/workflow/dispatch/tools/answer.ts index f11420cbb65..c8fc09ecd44 100644 --- a/packages/service/core/workflow/dispatch/tools/answer.ts +++ b/packages/service/core/workflow/dispatch/tools/answer.ts @@ -20,7 +20,7 @@ export const dispatchAnswer = (props: Record): AnswerResponse => { } = props as AnswerProps; const formatText = typeof text === 'string' ? text : JSON.stringify(text, null, 2); - const responseText = `\n${formatText}`; + const responseText = `\n${formatText}`.replaceAll('\\n', '\n'); workflowStreamResponse?.({ event: SseResponseEventEnum.fastAnswer, diff --git a/packages/web/components/common/EmptyTip/index.tsx b/packages/web/components/common/EmptyTip/index.tsx index 80b06955826..e8f450281b0 100644 --- a/packages/web/components/common/EmptyTip/index.tsx +++ b/packages/web/components/common/EmptyTip/index.tsx @@ -5,13 +5,14 @@ import { useTranslation } from 'next-i18next'; type Props = FlexProps & { text?: string | React.ReactNode; + iconSize?: string | number; }; -const EmptyTip = ({ text, ...props }: Props) => { +const EmptyTip = ({ text, iconSize = '48px', ...props }: Props) => { const { t } = useTranslation(); return ( - + {text || t('common:common.empty.Common Tip')} diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index d3ee88a17f5..00fd25f5a93 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -177,6 +177,7 @@ export const iconPaths = { 'core/workflow/debugResult': () => import('./icons/core/workflow/debugResult.svg'), 'core/workflow/edgeArrow': () => import('./icons/core/workflow/edgeArrow.svg'), 'core/workflow/grout': () => import('./icons/core/workflow/grout.svg'), + 'core/workflow/inputType/array': () => import('./icons/core/workflow/inputType/array.svg'), 'core/workflow/inputType/customVariable': () => import('./icons/core/workflow/inputType/customVariable.svg'), 'core/workflow/inputType/dynamic': () => import('./icons/core/workflow/inputType/dynamic.svg'), @@ -226,6 +227,9 @@ export const iconPaths = { 'core/workflow/template/ifelse': () => import('./icons/core/workflow/template/ifelse.svg'), 'core/workflow/template/lafDispatch': () => import('./icons/core/workflow/template/lafDispatch.svg'), + 'core/workflow/template/loop': () => import('./icons/core/workflow/template/loop.svg'), + 'core/workflow/template/loopEnd': () => import('./icons/core/workflow/template/loopEnd.svg'), + 'core/workflow/template/loopStart': () => import('./icons/core/workflow/template/loopStart.svg'), 'core/workflow/template/mathCall': () => import('./icons/core/workflow/template/mathCall.svg'), 'core/workflow/template/pluginOutput': () => import('./icons/core/workflow/template/pluginOutput.svg'), diff --git a/packages/web/components/common/Icon/icons/core/workflow/inputType/array.svg b/packages/web/components/common/Icon/icons/core/workflow/inputType/array.svg new file mode 100644 index 00000000000..9e3bf1b2df7 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/inputType/array.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/loop.svg b/packages/web/components/common/Icon/icons/core/workflow/template/loop.svg new file mode 100644 index 00000000000..548dba9a7b6 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loop.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg b/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg new file mode 100644 index 00000000000..ca8fc3c9145 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg b/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg new file mode 100644 index 00000000000..546a3ecd5df --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/components/common/MySelect/MultipleRowSelect.tsx b/packages/web/components/common/MySelect/MultipleRowSelect.tsx index c3efd53ba93..73f630bf016 100644 --- a/packages/web/components/common/MySelect/MultipleRowSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleRowSelect.tsx @@ -13,6 +13,7 @@ const MultipleRowSelect = ({ emptyTip, maxH = 300, onSelect, + popDirection = 'bottom', styles }: MultipleSelectProps) => { const { t } = useTranslation(); @@ -124,7 +125,13 @@ const MultipleRowSelect = ({ {isOpen && ( = { maxH?: number; onSelect: (val: any[]) => void; styles?: ButtonProps; + popDirection?: 'top' | 'bottom'; }; diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx index 03158b4fd63..45632b03edc 100644 --- a/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx @@ -101,7 +101,7 @@ export default function VariablePickerPlugin({ {item.key} - {item.key !== item.label && `(${item.label})`} + {item.key !== item.label && `(${t(item.label as any)})`} ))} diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index e73c180d774..33895e953fc 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -552,7 +552,11 @@ "search using reRank": "Result Re-Rank", "text output": "Text Output", "update_var_result": "Variable Update Result (Displays Multiple Variable Update Results in Order)", - "user_select_result": "User Selection Result" + "user_select_result": "User Selection Result", + "loop_input": "Loop Input Array", + "loop_output": "Loop Output Array", + "loop_input_element": "Loop Input Element", + "loop_output_element": "Loop Output Element" }, "retry": "Regenerate", "tts": { diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index c3c166f8e23..5fee7a5485e 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -1,10 +1,12 @@ { + "Array_element": "Array element", "Code": "Code", "about_xxx_question": "Question regarding xxx", "add_new_input": "Add New Input", "append_application_reply_to_history_as_new_context": "Append the application's reply to the history as new context", "application_call": "Application Call", "assigned_reply": "Assigned Reply", + "can_not_loop": "This node can't loop.", "choose_another_application_to_call": "Select another application to call", "classification_result": "Classification Result", "code": { @@ -88,6 +90,8 @@ "length_not_equal_to": "Length Not Equal To", "less_than": "Less Than", "less_than_or_equal_to": "Less Than or Equal To", + "loop": "Batch execution", + "loop_start_tip": "Not input array", "max_dialog_rounds": "Maximum Number of Dialog Rounds", "max_tokens": "Maximum Tokens", "mouse_priority": "Mouse first", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index e7d2ced721f..77bf7a80b6d 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -552,7 +552,11 @@ "search using reRank": "结果重排", "text output": "文本输出", "update_var_result": "变量更新结果(按顺序展示多个变量更新结果)", - "user_select_result": "用户选择结果" + "user_select_result": "用户选择结果", + "loop_input": "输入数组", + "loop_output": "输出数组", + "loop_input_element": "输入数组元素", + "loop_output_element": "输出数组元素" }, "retry": "重新生成", "tts": { diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index f67c152c2ac..36faed658e5 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -1,10 +1,12 @@ { + "Array_element": "数组元素", "Code": "代码", "about_xxx_question": "关于 xxx 的问题", "add_new_input": "新增输入", "append_application_reply_to_history_as_new_context": "将该应用回复内容拼接到历史记录中,作为新的上下文返回", "application_call": "应用调用", "assigned_reply": "指定回复", + "can_not_loop": "该节点不支持循环嵌套", "choose_another_application_to_call": "选择一个其他应用进行调用", "classification_result": "分类结果", "code": { @@ -66,6 +68,7 @@ "intro_http_request": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)", "intro_knowledge_base_search_merge": "可以将多个知识库搜索结果进行合并输出。使用 RRF 的合并方式进行最终排序输出。", "intro_laf_function_call": "可以调用Laf账号下的云函数。", + "intro_loop": "可以输入一个数组,数组内元素将独立执行循环体,并将所有结果作为数组输出。", "intro_plugin_input": "可以配置插件需要哪些输入,利用这些输入来运行插件", "intro_question_classification": "根据用户的历史记录和当前问题判断该次提问的类型。可以添加多组问题类型,下面是一个模板例子:\n类型1: 打招呼\n类型2: 关于商品“使用”问题\n类型3: 关于商品“购买”问题\n类型4: 其他问题", "intro_question_optimization": "使用问题优化功能,可以提高知识库连续对话时搜索的精度。使用该功能后,会先利用 AI 根据上下文构建一个或多个新的检索词,这些检索词更利于进行知识库搜索。该模块已内置在知识库搜索模块中,如果您仅进行一次知识库搜索,可直接使用知识库内置的补全功能。", @@ -88,6 +91,13 @@ "length_not_equal_to": "长度不等于", "less_than": "小于", "less_than_or_equal_to": "小于等于", + "loop": "批量执行(测试)", + "loop_body": "循环体", + "loop_end": "循环体结束", + "loop_input_array": "数组", + "loop_result": "数组执行结果", + "loop_start": "循环体开始", + "loop_start_tip": "未输入数组", "max_dialog_rounds": "最多携带多少轮对话记录", "max_tokens": "最大 Tokens", "mouse_priority": "鼠标优先", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69cd7d90f58..8fc0382d1af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 13.3.0 next-i18next: specifier: 15.3.0 - version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) prettier: specifier: 3.2.4 version: 3.2.4 @@ -63,8 +63,8 @@ importers: specifier: 14.2.5 version: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) openai: - specifier: 4.57.0 - version: 4.57.0(encoding@0.1.13) + specifier: 4.61.0 + version: 4.61.0(encoding@0.1.13) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -250,7 +250,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.1.5 - version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@chakra-ui/react': specifier: 2.8.1 version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -313,7 +313,7 @@ importers: version: 4.17.21 next-i18next: specifier: 15.3.0 - version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -374,7 +374,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.1.5 - version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@chakra-ui/react': specifier: 2.8.1 version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -470,7 +470,7 @@ importers: version: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) next-i18next: specifier: 15.3.0 - version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nextjs-node-loader: specifier: ^1.1.5 version: 1.1.5(webpack@5.92.1) @@ -6992,8 +6992,8 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} - openai@4.57.0: - resolution: {integrity: sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==} + openai@4.61.0: + resolution: {integrity: sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==} hasBin: true peerDependencies: zod: ^3.23.8 @@ -10087,7 +10087,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': dependencies: '@chakra-ui/react': 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/cache': 11.11.0 @@ -16690,7 +16690,7 @@ snapshots: neo-async@2.6.2: {} - next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.8 '@types/hoist-non-react-statics': 3.3.5 @@ -16887,7 +16887,7 @@ snapshots: dependencies: mimic-fn: 4.0.0 - openai@4.57.0(encoding@0.1.13): + openai@4.61.0(encoding@0.1.13): dependencies: '@types/node': 18.19.40 '@types/node-fetch': 2.6.11 diff --git a/projects/app/data/config.json b/projects/app/data/config.json index fcba250ebb8..dbe489918d4 100644 --- a/projects/app/data/config.json +++ b/projects/app/data/config.json @@ -54,6 +54,60 @@ "customExtractPrompt": "", "defaultSystemChatPrompt": "", "defaultConfig": {} + }, + { + "model": "o1-mini", + "name": "o1-mini", + "avatar": "/imgs/model/openai.svg", + "maxContext": 125000, + "maxResponse": 4000, + "quoteMaxToken": 120000, + "maxTemperature": 1.2, + "charsPointsPrice": 0, + "censor": false, + "vision": false, + "datasetProcess": false, + "usedInClassify": true, + "usedInExtractFields": true, + "usedInToolCall": true, + "usedInQueryExtension": true, + "toolChoice": false, + "functionCall": false, + "customCQPrompt": "", + "customExtractPrompt": "", + "defaultSystemChatPrompt": "", + "defaultConfig": { + "temperature": 1, + "max_tokens": null, + "stream": false + } + }, + { + "model": "o1-preview", + "name": "o1-preview", + "avatar": "/imgs/model/openai.svg", + "maxContext": 125000, + "maxResponse": 4000, + "quoteMaxToken": 120000, + "maxTemperature": 1.2, + "charsPointsPrice": 0, + "censor": false, + "vision": false, + "datasetProcess": false, + "usedInClassify": true, + "usedInExtractFields": true, + "usedInToolCall": true, + "usedInQueryExtension": true, + "toolChoice": false, + "functionCall": false, + "customCQPrompt": "", + "customExtractPrompt": "", + "defaultSystemChatPrompt": "", + "defaultConfig": { + "temperature": 1, + "max_tokens": null, + "stream": false + } } ], "vectorModels": [ diff --git a/projects/app/src/components/Markdown/index.tsx b/projects/app/src/components/Markdown/index.tsx index 9f1fe26250a..9d0c9b7cf01 100644 --- a/projects/app/src/components/Markdown/index.tsx +++ b/projects/app/src/components/Markdown/index.tsx @@ -39,7 +39,6 @@ const Markdown = ({ () => ({ img: Image, pre: RewritePre, - p: (pProps: any) =>

, code: Code, a: A }), diff --git a/projects/app/src/components/common/NextHead/index.tsx b/projects/app/src/components/common/NextHead/index.tsx index 580b131ab4b..d6210cff4be 100644 --- a/projects/app/src/components/common/NextHead/index.tsx +++ b/projects/app/src/components/common/NextHead/index.tsx @@ -1,7 +1,16 @@ +import { LOGO_ICON } from '@fastgpt/global/common/system/constants'; import Head from 'next/head'; -import React from 'react'; +import React, { useMemo } from 'react'; const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?: string }) => { + const formatIcon = useMemo(() => { + if (!icon) return LOGO_ICON; + if (icon.startsWith('http') || icon.startsWith('/')) { + return icon; + } + return LOGO_ICON; + }, [icon]); + return ( {title} @@ -11,7 +20,7 @@ const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?: /> {desc && } - {icon && } + {icon && } ); }; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx index 4f62fd2ce31..00a859b59f3 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx @@ -504,7 +504,6 @@ const ChatInput = ({ const files = Array.from(items) .map((item) => (item.kind === 'file' ? item.getAsFile() : undefined)) .filter((file) => { - console.log(file); return file && fileTypeFilter(file); }) as File[]; onSelectFile(files); diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index c67dd9ab1e1..60732856d55 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -35,8 +35,7 @@ const RenderText = React.memo(function RenderText({ showAnimation: boolean; text?: string; }) { - let source = (text || '').trim(); - + let source = text || ''; // First empty line // if (!source && !isLastChild) return null; diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index b5c8a19c961..7c793d7b568 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -8,7 +8,6 @@ import Markdown from '@/components/Markdown'; import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal'; import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants'; import { formatNumber } from '@fastgpt/global/common/math/tools'; -import { useI18n } from '@/web/context/I18n'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; @@ -337,6 +336,22 @@ export const WholeResponseContent = ({ label={t('common:core.chat.response.update_var_result')} value={activeModule?.updateVarResult} /> + + {/* loop */} + + + + {/* loopStart */} + + + {/* loopEnd */} + ) : null; }; @@ -525,6 +540,9 @@ export const ResponseBox = React.memo(function ResponseBox({ if (Array.isArray(item.pluginDetail)) { helper(item.pluginDetail); } + if (Array.isArray(item.loopDetail)) { + helper(item.loopDetail); + } } }); } @@ -552,9 +570,10 @@ export const ResponseBox = React.memo(function ResponseBox({ function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] { return res.map((item) => { let children: sideTabItemType[] = []; - if (!!(item?.toolDetail || item?.pluginDetail)) { + if (!!(item?.toolDetail || item?.pluginDetail || item?.loopDetail)) { if (item?.toolDetail) children.push(...pretreatmentResponse(item?.toolDetail)); if (item?.pluginDetail) children.push(...pretreatmentResponse(item?.pluginDetail)); + if (item?.loopDetail) children.push(...pretreatmentResponse(item?.loopDetail)); } return { @@ -611,7 +630,7 @@ export const ResponseBox = React.memo(function ResponseBox({ <> {isPc && !useMobile ? ( - + - + { data: invoices, isLoading, Pagination, - getData, total } = usePagination({ api: getInvoiceRecords, - pageSize: 20, - defaultRequest: false + pageSize: 20 }); - useEffect(() => { - getData(1); - }, [getData]); - return ( diff --git a/projects/app/src/pages/api/core/chat/getHistories.ts b/projects/app/src/pages/api/core/chat/getHistories.ts index 19ecbb0112f..644da2d4f3f 100644 --- a/projects/app/src/pages/api/core/chat/getHistories.ts +++ b/projects/app/src/pages/api/core/chat/getHistories.ts @@ -18,69 +18,67 @@ async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise> { - try { - await connectToDatabase(); - const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } = - req.body as getHistoriesBody; + const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } = + req.body as getHistoriesBody; - const limit = shareId && outLinkUid ? 20 : 30; + const match = await (async () => { + if (shareId && outLinkUid) { + const { uid } = await authOutLink({ shareId, outLinkUid }); - const match = await (async () => { - if (shareId && outLinkUid) { - const { uid } = await authOutLink({ shareId, outLinkUid }); - - return { - shareId, - outLinkUid: uid, - source: ChatSourceEnum.share, - updateTime: { - $gte: new Date(new Date().setDate(new Date().getDate() - 30)) - } - }; - } - if (appId && teamId && teamToken) { - const { uid } = await authTeamSpaceToken({ teamId, teamToken }); - return { - teamId, - appId, - outLinkUid: uid, - source: ChatSourceEnum.team - }; - } - if (appId) { - const { tmbId } = await authCert({ req, authToken: true }); - return { - tmbId, - appId, - source: ChatSourceEnum.online - }; - } - - return Promise.reject('Params are error'); - })(); - - const [data, total] = await Promise.all([ - await MongoChat.find(match, 'chatId title top customTitle appId updateTime') - .sort({ top: -1, updateTime: -1 }) - .skip((current - 1) * pageSize) - .limit(pageSize), - MongoChat.countDocuments(match) - ]); + return { + shareId, + outLinkUid: uid, + source: ChatSourceEnum.share, + updateTime: { + $gte: new Date(new Date().setDate(new Date().getDate() - 30)) + } + }; + } + if (appId && teamId && teamToken) { + const { uid } = await authTeamSpaceToken({ teamId, teamToken }); + return { + teamId, + appId, + outLinkUid: uid, + source: ChatSourceEnum.team + }; + } + if (appId) { + const { tmbId } = await authCert({ req, authToken: true }); + return { + tmbId, + appId, + source: ChatSourceEnum.online + }; + } + })(); + if (!match) { return { - list: data.map((item) => ({ - chatId: item.chatId, - updateTime: item.updateTime, - appId: item.appId, - customTitle: item.customTitle, - title: item.title, - top: item.top - })), - total + list: [], + total: 0 }; - } catch (err) { - return Promise.reject(err); } + + const [data, total] = await Promise.all([ + await MongoChat.find(match, 'chatId title top customTitle appId updateTime') + .sort({ top: -1, updateTime: -1 }) + .skip((current - 1) * pageSize) + .limit(pageSize), + MongoChat.countDocuments(match) + ]); + + return { + list: data.map((item) => ({ + chatId: item.chatId, + updateTime: item.updateTime, + appId: item.appId, + customTitle: item.customTitle, + title: item.title, + top: item.top + })), + total + }; } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index de00134f57e..c056deb716d 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -20,6 +20,7 @@ async function handler( const { parentId, name, + intro, type = DatasetTypeEnum.dataset, avatar, vectorModel = global.vectorModels[0].model, @@ -47,6 +48,7 @@ async function handler( const { _id } = await MongoDataset.create({ ...parseParentIdInMongo(parentId), name, + intro, teamId, tmbId, vectorModel, diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 80fd0de7a48..1a0550d594a 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -3,11 +3,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { sseErrRes, jsonRes } from '@fastgpt/service/common/response'; import { addLog } from '@fastgpt/service/common/system/log'; -import { - ChatItemValueTypeEnum, - ChatRoleEnum, - ChatSourceEnum -} from '@fastgpt/global/core/chat/constants'; +import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d'; diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index 632625e2487..c900a43d3d5 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils'; import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useDebounceEffect } from 'ahooks'; const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider')); @@ -66,26 +67,32 @@ const Header = () => { setPast } = useContextSelector(WorkflowContext, (v) => v); - const isPublished = useMemo(() => { - /* - Find the last saved snapshot in the past and future snapshots - */ - const savedSnapshot = - future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + const [isPublished, setIsPublished] = useState(false); + useDebounceEffect( + () => { + const savedSnapshot = + future.findLast((snapshot) => snapshot.isSaved) || + past.find((snapshot) => snapshot.isSaved); - return compareSnapshot( - { - nodes: savedSnapshot?.nodes, - edges: savedSnapshot?.edges, - chatConfig: savedSnapshot?.chatConfig - }, - { - nodes: nodes, - edges: edges, - chatConfig: appDetail.chatConfig - } - ); - }, [future, past, nodes, edges, appDetail.chatConfig]); + const val = compareSnapshot( + { + nodes: savedSnapshot?.nodes, + edges: savedSnapshot?.edges, + chatConfig: savedSnapshot?.chatConfig + }, + { + nodes: nodes, + edges: edges, + chatConfig: appDetail.chatConfig + } + ); + setIsPublished(val); + }, + [future, past, nodes, edges, appDetail.chatConfig], + { + wait: 500 + } + ); const { runAsync: onClickSave, loading } = useRequest2( async ({ @@ -205,7 +212,7 @@ const Header = () => { size={'sm'} leftIcon={} variant={'whitePrimary'} - onClick={async () => { + onClick={() => { const data = flowData2StoreDataAndCheck(); if (data) { setWorkflowTestData(data); diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx index 49afc0b8c95..567fb640eec 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx @@ -124,7 +124,7 @@ const EditForm = ({ const selectedModel = getWebLLMModel(appForm.aiSettings.model); const tokenLimit = useMemo(() => { return selectedModel?.quoteMaxToken || 3000; - }, [selectedModel.quoteMaxToken]); + }, [selectedModel?.quoteMaxToken]); return ( <> @@ -343,7 +343,7 @@ const EditForm = ({ {/* File select */} { setAppForm((state) => ({ diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index 632625e2487..3e5925d0603 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Box, Flex, @@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils'; import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useDebounceEffect } from 'ahooks'; const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider')); @@ -66,26 +67,33 @@ const Header = () => { setPast } = useContextSelector(WorkflowContext, (v) => v); - const isPublished = useMemo(() => { - /* - Find the last saved snapshot in the past and future snapshots - */ - const savedSnapshot = - future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + // Check if the workflow is published + const [isPublished, setIsPublished] = useState(false); + useDebounceEffect( + () => { + const savedSnapshot = + future.findLast((snapshot) => snapshot.isSaved) || + past.find((snapshot) => snapshot.isSaved); - return compareSnapshot( - { - nodes: savedSnapshot?.nodes, - edges: savedSnapshot?.edges, - chatConfig: savedSnapshot?.chatConfig - }, - { - nodes: nodes, - edges: edges, - chatConfig: appDetail.chatConfig - } - ); - }, [future, past, nodes, edges, appDetail.chatConfig]); + const val = compareSnapshot( + { + nodes: savedSnapshot?.nodes, + edges: savedSnapshot?.edges, + chatConfig: savedSnapshot?.chatConfig + }, + { + nodes: nodes, + edges: edges, + chatConfig: appDetail.chatConfig + } + ); + setIsPublished(val); + }, + [future, past, nodes, edges, appDetail.chatConfig], + { + wait: 500 + } + ); const { runAsync: onClickSave, loading } = useRequest2( async ({ @@ -205,7 +213,7 @@ const Header = () => { size={'sm'} leftIcon={} variant={'whitePrimary'} - onClick={async () => { + onClick={() => { const data = flowData2StoreDataAndCheck(); if (data) { setWorkflowTestData(data); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx index 000d4c67425..e3db7a4da6e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'next-i18next'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import { useI18n } from '@/web/context/I18n'; import { WorkflowContext } from './context'; import { filterSensitiveNodesData } from '@/web/core/workflow/utils'; import dynamic from 'next/dynamic'; @@ -28,7 +27,6 @@ const AppCard = ({ isPublished: boolean; }) => { const { t } = useTranslation(); - const { appT } = useI18n(); const { feConfigs } = useSystemStore(); const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } = @@ -48,7 +46,7 @@ const AppCard = ({ children: [ { icon: 'edit', - label: appT('edit_info'), + label: t('app:edit_info'), onClick: onOpenInfoEdit }, { @@ -63,7 +61,7 @@ const AppCard = ({ { children: [ { - label: appT('import_configs'), + label: t('app:import_configs'), icon: 'common/importLight', onClick: onOpenImport }, @@ -117,7 +115,6 @@ const AppCard = ({ appDetail.name, appDetail.permission.hasWritePer, appDetail.permission.isOwner, - appT, currentTab, feConfigs?.show_team_chat, historiesDefaultData, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx index 1966d19e55c..dd67eb8f024 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx @@ -69,7 +69,6 @@ const ImportSettings = ({ onClose }: Props) => { async (e: File[]) => { const file = e[0]; readJSONFile(file); - console.log(file); }, [readJSONFile] ); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx index e478859ec7f..c0242032e92 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Box, Flex, @@ -14,7 +14,7 @@ import type { NodeTemplateListItemType, NodeTemplateListType } from '@fastgpt/global/core/workflow/type/node.d'; -import { useViewport, XYPosition } from 'reactflow'; +import { useReactFlow, XYPosition } from 'reactflow'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils'; @@ -36,9 +36,7 @@ import { useRouter } from 'next/router'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../context'; -import { useI18n } from '@/web/context/I18n'; import { getTeamPlugTemplates } from '@/web/core/app/api/plugin'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import MyBox from '@fastgpt/web/components/common/MyBox'; import FolderPath from '@/components/common/folder/Path'; @@ -49,6 +47,8 @@ import { cloneDeep } from 'lodash'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import CostTooltip from '@/components/core/app/plugin/CostTooltip'; import { useUserStore } from '@/web/support/user/useUserStore'; +import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart'; +import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd'; type ModuleTemplateListProps = { isOpen: boolean; @@ -397,7 +397,7 @@ const RenderList = React.memo(function RenderList({ const { isPc } = useSystem(); const isSystemPlugin = type === TemplateTypeEnum.systemPlugin; - const { x, y, zoom } = useViewport(); + const { screenToFlowPosition } = useReactFlow(); const { toast } = useToast(); const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper); const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes); @@ -454,11 +454,11 @@ const RenderList = React.memo(function RenderList({ } })(); - const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); - const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100; - const mouseY = (position.y - reactFlowBounds.top - y) / zoom; + const nodePosition = screenToFlowPosition(position); + const mouseX = nodePosition.x - 100; + const mouseY = nodePosition.y - 20; - const node = nodeTemplate2FlowNode({ + const newNode = nodeTemplate2FlowNode({ template: { ...templateNode, name: computedNewNodeName({ @@ -482,9 +482,28 @@ const RenderList = React.memo(function RenderList({ description: t(output.description as any) })) }, - position: { x: mouseX, y: mouseY - 20 }, - selected: true + position: { x: mouseX, y: mouseY }, + selected: true, + t }); + const newNodes = [newNode]; + + if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) { + const startNode = nodeTemplate2FlowNode({ + template: LoopStartNode, + position: { x: mouseX + 60, y: mouseY + 280 }, + parentNodeId: newNode.id, + t + }); + const endNode = nodeTemplate2FlowNode({ + template: LoopEndNode, + position: { x: mouseX + 420, y: mouseY + 680 }, + parentNodeId: newNode.id, + t + }); + + newNodes.push(startNode, endNode); + } setNodes((state) => { const newState = state @@ -493,11 +512,11 @@ const RenderList = React.memo(function RenderList({ selected: false })) // @ts-ignore - .concat(node); + .concat(newNodes); return newState; }); }, - [computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom] + [computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, screenToFlowPosition] ); const gridStyle = useMemo(() => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx index 428edf478ac..0f0e956f0ab 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx @@ -7,7 +7,7 @@ import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; const ButtonEdge = (props: EdgeProps) => { - const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector( + const { nodes, nodeList, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector( WorkflowContext, (v) => v ); @@ -27,6 +27,10 @@ const ButtonEdge = (props: EdgeProps) => { targetHandleId, style } = props; + const defaultZIndex = useMemo( + () => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0), + [nodeList, source] + ); const onDelConnect = useCallback( (id: string) => { @@ -135,7 +139,7 @@ const ButtonEdge = (props: EdgeProps) => { bg={'white'} borderRadius={'17px'} cursor={'pointer'} - zIndex={1000} + zIndex={9999} onClick={() => onDelConnect(id)} > @@ -150,7 +154,7 @@ const ButtonEdge = (props: EdgeProps) => { w={highlightEdge ? '14px' : '10px'} h={highlightEdge ? '14px' : '10px'} // bg={'white'} - zIndex={highlightEdge ? 1000 : 0} + zIndex={highlightEdge ? 1000 : defaultZIndex} > { return { ...style, - strokeWidth: 3, - zIndex: 2 + strokeWidth: 3 }; })(); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/Container.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/Container.tsx index 47279ca3b43..90b38e6dbf5 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/Container.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/Container.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Box } from '@chakra-ui/react'; +import { Flex } from '@chakra-ui/react'; import { BoxProps } from '@chakra-ui/react'; const Container = ({ children, ...props }: BoxProps) => { return ( - { {...props} > {children} - + ); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx index a65b5351a9f..475f10259c4 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx @@ -12,7 +12,7 @@ const IOTitle = ({ return ( - {text} + {text} {inputExplanationUrl && ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx index 63d48bc0218..824814d4de2 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx @@ -8,6 +8,7 @@ import { useContextSelector } from 'use-context-selector'; import { WorkflowContext, getWorkflowStore } from '../../context'; import { useWorkflowUtils } from './useUtils'; import { useKeyPress as useKeyPressEffect } from 'ahooks'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; export const useKeyboard = () => { const { t } = useTranslation(); @@ -50,7 +51,9 @@ export const useKeyboard = () => { if (!Array.isArray(parseData)) return; // filter workflow data const newNodes = parseData - .filter((item) => !!item.type && item.data?.unique !== true) + .filter( + (item) => !!item.type && item.data?.unique !== true && item.type !== FlowNodeTypeEnum.loop + ) .map((item) => { const nodeId = getNanoid(); return { @@ -64,7 +67,8 @@ export const useKeyboard = () => { flowNodeType: item.data?.flowNodeType || '', pluginId: item.data?.pluginId }), - nodeId + nodeId, + parentNodeId: undefined }, position: { x: item.position.x + 100, @@ -73,6 +77,7 @@ export const useKeyboard = () => { }; }); + // Reset all node to not select and concat new node setNodes((prev) => prev .map((node) => ({ diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index a5bf321e7e6..f38868dd0b7 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx @@ -8,9 +8,14 @@ import { Edge, Node, NodePositionChange, - XYPosition + XYPosition, + useReactFlow, + getNodesBounds, + Rect, + NodeRemoveChange, + NodeSelectionChange } from 'reactflow'; -import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant'; +import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import 'reactflow/dist/style.css'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useTranslation } from 'next-i18next'; @@ -18,11 +23,18 @@ import { useKeyboard } from './useKeyboard'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { useMemoizedFn } from 'ahooks'; +import { + Input_Template_Node_Height, + Input_Template_Node_Width +} from '@fastgpt/global/core/workflow/template/input'; +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; /* - Compute helper lines for snapping nodes to each other - Refer: https://reactflow.dev/examples/interaction/helper-lines - */ + Compute helper lines for snapping nodes to each other + Refer: https://reactflow.dev/examples/interaction/helper-lines +*/ type GetHelperLinesResult = { horizontal?: THelperLine; vertical?: THelperLine; @@ -259,18 +271,64 @@ export const useWorkflow = () => { const { t } = useTranslation(); const { isDowningCtrl } = useKeyboard(); - const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } = - useContextSelector(WorkflowContext, (v) => v); + const { + setConnectingEdge, + nodes, + onNodesChange, + setEdges, + onChangeNode, + onEdgesChange, + setHoverEdgeId + } = useContextSelector(WorkflowContext, (v) => v); + + const { getIntersectingNodes } = useReactFlow(); + + // Loop node size and position + const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => { + const width = rect.width + 110 > 900 ? rect.width + 110 : 900; + const height = rect.height + 380 > 900 ? rect.height + 380 : 900; + + // Update parentNode size and position + onChangeNode({ + nodeId: parentId, + type: 'updateInput', + key: NodeInputKeyEnum.nodeWidth, + value: { + ...Input_Template_Node_Width, + value: width + } + }); + onChangeNode({ + nodeId: parentId, + type: 'updateInput', + key: NodeInputKeyEnum.nodeHeight, + value: { + ...Input_Template_Node_Height, + value: height + } + }); + + // Update parentNode position + onNodesChange([ + { + id: parentId, + type: 'position', + position: { + x: rect.x - 50, + y: rect.y - 280 + } + } + ]); + }); /* helper line */ const [helperLineHorizontal, setHelperLineHorizontal] = useState(); const [helperLineVertical, setHelperLineVertical] = useState(); - const customApplyNodeChanges = (changes: NodeChange[], nodes: Node[]) => { - const positionChange = - changes[0].type === 'position' && changes[0].dragging ? changes[0] : undefined; + const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => { + const positionChange = change.type === 'position' && change.dragging ? change : undefined; - if (changes.length === 1 && positionChange?.position) { + if (positionChange?.position) { // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 const filterNodes = nodes .filter((node) => { @@ -303,42 +361,182 @@ export const useWorkflow = () => { setHelperLineHorizontal(undefined); setHelperLineVertical(undefined); } - }; + }); + + // Check if a node is placed on top of a loop node + const checkNodeOverLoopNode = useMemoizedFn((node: Node) => { + if (!node) return; + + // 获取所有与当前节点相交的节点 + const intersections = getIntersectingNodes(node); + // 获取所有与当前节点相交的节点中,类型为 loop 的节点 + const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop); + + const unSupportedTypes = [ + FlowNodeTypeEnum.workflowStart, + FlowNodeTypeEnum.loop, + FlowNodeTypeEnum.pluginInput, + FlowNodeTypeEnum.pluginOutput, + FlowNodeTypeEnum.systemConfig + ]; + + if (parentNode && !node.data.parentNodeId) { + if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) { + return toast({ + status: 'warning', + title: t('workflow:can_not_loop') + }); + } + + onChangeNode({ + nodeId: node.id, + type: 'attr', + key: 'parentNodeId', + value: parentNode.id + }); + // 删除当前节点与其他节点的连接 + setEdges((state) => + state.filter((edge) => edge.source !== node.id && edge.target !== node.id) + ); + + const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node]; + const rect = getNodesBounds(childNodes); + resetParentNodeSizeAndPosition(rect, parentNode.id); + } + }); /* node */ - const handleNodesChange = (changes: NodeChange[]) => { + const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, node: Node) => { + if (node.data.forbidDelete) { + return toast({ + status: 'warning', + title: t('common:core.workflow.Can not delete node') + }); + } + + // If the node has child nodes, remove the child nodes + if (nodes.some((n) => n.data.parentNodeId === node.id)) { + const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id); + const childNodeIds = childNodes.map((n) => n.id); + const childNodesChange = childNodes.map((node) => ({ + ...change, + id: node.id + })); + onNodesChange(childNodesChange); + setEdges((state) => + state.filter( + (edge) => + edge.source !== change.id && + edge.target !== change.id && + !childNodeIds.includes(edge.source) && + !childNodeIds.includes(edge.target) + ) + ); + return; + } + + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + }); + const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => { + // If the node is not selected and the Ctrl key is pressed, select the node + if (change.selected === false && isDowningCtrl) { + change.selected = true; + } + }); + const handlePositionNode = useMemoizedFn( + (change: NodePositionChange, node: Node) => { + const parentNode: Record = { + [FlowNodeTypeEnum.loop]: 1 + }; + + // If node is a child node, move child node and reset parent node + if (node.data.parentNodeId) { + const parentId = node.data.parentNodeId; + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); + checkNodeHelpLine(change, childNodes); + + resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId); + } + // If node is parent node, move parent node and child nodes + else if (parentNode[node.data.flowNodeType]) { + // It will update the change value. + checkNodeHelpLine( + change, + nodes.filter((node) => !node.data.parentNodeId) + ); + + // Compute the child nodes' position + const parentId = node.id; + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); + const initPosition = node.position; + const deltaX = change.position?.x ? change.position.x - initPosition.x : 0; + const deltaY = change.position?.y ? change.position.y - initPosition.y : 0; + const childNodesChange: NodePositionChange[] = childNodes.map((node) => { + if (change.dragging) { + const position = { + x: node.position.x + deltaX, + y: node.position.y + deltaY + }; + return { + ...change, + id: node.id, + position, + positionAbsolute: position + }; + } else { + return { + ...change, + id: node.id + }; + } + }); + + onNodesChange(childNodesChange); + } else { + checkNodeHelpLine( + change, + nodes.filter((node) => !node.data.parentNodeId) + ); + } + } + ); + const handleNodesChange = useMemoizedFn((changes: NodeChange[]) => { for (const change of changes) { if (change.type === 'remove') { const node = nodes.find((n) => n.id === change.id); - if (node && node.data.forbidDelete) { - return toast({ - status: 'warning', - title: t('common:core.workflow.Can not delete node') - }); - } else { - return (() => { - onNodesChange(changes); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) - ); - })(); + if (node) { + handleRemoveNode(change, node); + } + } else if (change.type === 'select') { + handleSelectNode(change); + } else if (change.type === 'position') { + const node = nodes.find((n) => n.id === change.id); + if (node) { + handlePositionNode(change, node); } - } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { - change.selected = true; } } - customApplyNodeChanges(changes, nodes); - + // default changes onNodesChange(changes); - }; + }); + const handleEdgeChange = useCallback( (changes: EdgeChange[]) => { - onEdgesChange(changes.filter((change) => change.type !== 'remove')); + onEdgesChange(changes); }, [onEdgesChange] ); + const onNodeDragStop = useCallback( + (_: any, node: Node) => { + checkNodeOverLoopNode(node); + }, + [checkNodeOverLoopNode] + ); + /* connect */ const onConnectStart = useCallback( (event: any, params: OnConnectStartParams) => { @@ -403,7 +601,8 @@ export const useWorkflow = () => { onEdgeMouseEnter, onEdgeMouseLeave, helperLineHorizontal, - helperLineVertical + helperLineVertical, + onNodeDragStop }; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx index 455ee74e37f..db7256dbb16 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx @@ -52,7 +52,10 @@ const nodeTypes: Record = { [FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')), [FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')), [FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode')), - [FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect')) + [FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect')), + [FlowNodeTypeEnum.loop]: dynamic(() => import('./nodes/Loop/NodeLoop')), + [FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/Loop/NodeLoopStart')), + [FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd')) }; const edgeTypes = { [EDGE_TYPE]: ButtonEdge @@ -73,7 +76,8 @@ const Workflow = () => { onEdgeMouseEnter, onEdgeMouseLeave, helperLineHorizontal, - helperLineVertical + helperLineVertical, + onNodeDragStop } = useWorkflow(); const { @@ -146,6 +150,7 @@ const Workflow = () => { panOnScroll: true } : {})} + onNodeDragStop={onNodeDragStop} > diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx new file mode 100644 index 00000000000..6e7762ea730 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -0,0 +1,88 @@ +/* + The loop node has controllable width and height properties, which serve as the parent node of loopFlow. + When the childNodes of loopFlow change, it automatically calculates the rectangular width, height, and position of the childNodes, + thereby further updating the width and height properties of the loop node. +*/ + +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import React, { useEffect, useMemo } from 'react'; +import { Background, NodeProps } from 'reactflow'; +import NodeCard from '../render/NodeCard'; +import Container from '../../components/Container'; +import IOTitle from '../../components/IOTitle'; +import { useTranslation } from 'react-i18next'; +import RenderInput from '../render/RenderInput'; +import { Box } from '@chakra-ui/react'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import RenderOutput from '../render/RenderOutput'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '../../../context'; + +const NodeLoop = ({ data, selected }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId, inputs, outputs } = data; + const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); + + const { nodeWidth, nodeHeight } = useMemo(() => { + return { + nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value, + nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value + }; + }, [inputs]); + const childrenNodeIdList = useMemo(() => { + return JSON.stringify( + nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId) + ); + }, [nodeId, nodeList]); + + useEffect(() => { + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.childrenNodeIdList, + value: { + ...Input_Template_Children_Node_List, + value: JSON.parse(childrenNodeIdList) + } + }); + }, [childrenNodeIdList]); + + const Render = useMemo(() => { + return ( + + + + + + + + {t('workflow:loop_body')} + + + + + + + + + + ); + }, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]); + + return Render; +}; + +export default React.memo(NodeLoop); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopEnd.tsx new file mode 100644 index 00000000000..04b962d3603 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopEnd.tsx @@ -0,0 +1,93 @@ +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { NodeProps } from 'reactflow'; +import NodeCard from '../render/NodeCard'; +import Reference from '../render/RenderInput/templates/Reference'; +import { Box } from '@chakra-ui/react'; +import React, { useEffect, useMemo } from 'react'; +import { + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '@fastgpt/global/core/workflow/constants'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '../../../context'; +import { AppContext } from '../../../../context'; +import { useTranslation } from 'react-i18next'; +import { getGlobalVariableNode } from '@/web/core/workflow/adapt'; + +const typeMap = { + [WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString, + [WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber, + [WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean, + [WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny +}; + +const NodeLoopEnd = ({ data, selected }: NodeProps) => { + const { nodeId, inputs, parentNodeId } = data; + const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); + const { appDetail } = useContextSelector(AppContext, (v) => v); + const { t } = useTranslation(); + + const inputItem = useMemo( + () => inputs.find((input) => input.key === NodeInputKeyEnum.loopEndInput), + [inputs] + ); + + // Get loopEnd input value type + const valueType = useMemo(() => { + if (!inputItem) return; + + const referenceNode = [ + ...nodeList, + getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig }) + ].find((node) => node.nodeId === inputItem.value[0]); + + return referenceNode?.outputs.find((output) => output.id === inputItem?.value[1]) + ?.valueType as keyof typeof typeMap; + }, [appDetail.chatConfig, inputItem, nodeList, t]); + + useEffect(() => { + if (!valueType) return; + + const parentNode = nodeList.find((node) => node.nodeId === parentNodeId); + const parentNodeOutput = parentNode?.outputs.find( + (output) => output.key === NodeOutputKeyEnum.loopArray + ); + + if (parentNode && parentNodeOutput) { + onChangeNode({ + nodeId: parentNode.nodeId, + type: 'updateOutput', + key: NodeOutputKeyEnum.loopArray, + value: { + ...parentNodeOutput, + valueType: typeMap[valueType] ?? WorkflowIOValueTypeEnum.arrayAny + } + }); + } + }, [valueType, nodeList, nodeId, onChangeNode, parentNodeId]); + + const Render = useMemo(() => { + return ( + + + {inputItem && } + + + ); + }, [data, inputItem, nodeId, selected]); + + return Render; +}; + +export default React.memo(NodeLoopEnd); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx new file mode 100644 index 00000000000..2afe918f898 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx @@ -0,0 +1,148 @@ +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { useTranslation } from 'react-i18next'; +import { NodeProps } from 'reactflow'; +import NodeCard from '../render/NodeCard'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '../../../context'; +import { + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '@fastgpt/global/core/workflow/constants'; +import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; +import React, { useEffect, useMemo } from 'react'; +import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import MyIcon from '@fastgpt/web/components/common/Icon'; + +const typeMap = { + [WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string, + [WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number, + [WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean, + [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object, + [WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any +}; + +const NodeLoopStart = ({ data, selected }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId } = data; + const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); + + const loopStartNode = useMemo( + () => nodeList.find((node) => node.nodeId === nodeId), + [nodeList, nodeId] + ); + + // According to the variable referenced by parentInput, find the output of the corresponding node and take its output valueType + const loopItemInputType = useMemo(() => { + const parentNode = nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId); + const parentArrayInput = parentNode?.inputs.find( + (input) => input.key === NodeInputKeyEnum.loopInputArray + ); + return parentArrayInput?.value + ? (nodeList + .find((node) => node.nodeId === parentArrayInput?.value[0]) + ?.outputs.find((output) => output.id === parentArrayInput?.value[1]) + ?.valueType as keyof typeof typeMap) + : undefined; + }, [loopStartNode?.parentNodeId, nodeList]); + + // Auth update loopStartInput output + useEffect(() => { + const loopArrayOutput = loopStartNode?.outputs.find( + (output) => output.key === NodeOutputKeyEnum.loopStartInput + ); + + // if loopItemInputType is undefined, delete loopStartInput output + if (!loopItemInputType && loopArrayOutput) { + onChangeNode({ + nodeId, + type: 'delOutput', + key: NodeOutputKeyEnum.loopStartInput + }); + } + // if loopItemInputType is not undefined, and has no loopArrayOutput, add loopStartInput output + if (loopItemInputType && !loopArrayOutput) { + onChangeNode({ + nodeId, + type: 'addOutput', + value: { + id: NodeOutputKeyEnum.loopStartInput, + key: NodeOutputKeyEnum.loopStartInput, + label: t('workflow:Array_element'), + type: FlowNodeOutputTypeEnum.static, + valueType: typeMap[loopItemInputType as keyof typeof typeMap] + } + }); + } + // if loopItemInputType is not undefined, and has loopArrayOutput, update loopStartInput output + if (loopItemInputType && loopArrayOutput) { + onChangeNode({ + nodeId, + type: 'updateOutput', + key: NodeOutputKeyEnum.loopStartInput, + value: { + ...loopArrayOutput, + valueType: typeMap[loopItemInputType as keyof typeof typeMap] + } + }); + } + }, [loopStartNode?.outputs, nodeId, onChangeNode, loopItemInputType, t]); + + const Render = useMemo(() => { + return ( + + + {!loopItemInputType ? ( + + ) : ( + + + + + + + + + + + + + + + +
+ {t('common:core.module.variable.variable name')} + {t('common:core.workflow.Value type')}
+ + + {t('workflow:Array_element')} + + {typeMap[loopItemInputType]}
+
+
+ )} +
+
+ ); + }, [data, loopItemInputType, selected, t]); + + return Render; +}; + +export default React.memo(NodeLoopStart); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeAnswer.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeAnswer.tsx index ddeba35acb8..413caa5c8d2 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeAnswer.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeAnswer.tsx @@ -1,34 +1,37 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import Container from '../components/Container'; import RenderInput from './render/RenderInput'; import RenderToolInput from './render/RenderToolInput'; -import { useTranslation } from 'next-i18next'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; const NodeAnswer = ({ data, selected }: NodeProps) => { - const { t } = useTranslation(); const { nodeId, inputs } = data; const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs); - const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); - return ( - - - {isTool && ( - <> - - - - - )} - - {/* */} - - - ); + const Render = useMemo(() => { + const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); + + return ( + + + {isTool && ( + <> + + + + + )} + + {/* */} + + + ); + }, [splitToolInputs, inputs, nodeId, selected, data]); + + return Render; }; export default React.memo(NodeAnswer); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCQNode.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCQNode.tsx index e58209af811..8479afd6048 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCQNode.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCQNode.tsx @@ -130,12 +130,16 @@ const NodeCQNode = ({ data, selected }: NodeProps) => { [nodeId, onChangeNode, t] ); - return ( - - - - - - ); + const Render = useMemo(() => { + return ( + + + + + + ); + }, [CustomComponent, data, inputs, nodeId, selected]); + + return Render; }; export default React.memo(NodeCQNode); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCode.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCode.tsx index 0bb269c2333..7a298e9c2ca 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCode.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeCode.tsx @@ -23,11 +23,11 @@ const NodeCode = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { workflowT } = useI18n(); const { nodeId, inputs, outputs } = data; - const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs); - const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode); + const { splitToolInputs, onChangeNode, onResetNode } = useContextSelector( + WorkflowContext, + (ctx) => ctx + ); - const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); const { ConfirmModal, openConfirm } = useConfirm({ content: workflowT('code.Reset template confirm') }); @@ -73,31 +73,37 @@ const NodeCode = ({ data, selected }: NodeProps) => { ); } }; - }, [nodeId, onChangeNode, openConfirm, workflowT]); + }, [data, nodeId, onChangeNode, onResetNode, openConfirm, workflowT]); - return ( - - {isTool && ( - <> - - - - - )} - - - - - - - - - - - ); + const Render = useMemo(() => { + const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); + + return ( + + {isTool && ( + <> + + + + + )} + + + + + + + + + + + ); + }, [ConfirmModal, CustomComponent, data, inputs, nodeId, outputs, selected, splitToolInputs, t]); + + return Render; }; export default React.memo(NodeCode); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx index 5d3395c0c4e..42dd394a792 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeDatasetConcat.tsx @@ -26,33 +26,110 @@ import ValueTypeLabel from './render/ValueTypeLabel'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart'; import { getWebLLMModel } from '@/web/common/system/utils'; +import { useMemoizedFn } from 'ahooks'; const NodeDatasetConcat = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { llmModelList } = useSystemStore(); const { nodeId, inputs, outputs } = data; - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); + const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); - const quoteList = useMemo(() => inputs.filter((item) => item.canEdit), [inputs]); + const Reference = useMemoizedFn( + ({ nodeId, inputChildren }: { nodeId: string; inputChildren: FlowNodeInputItemType }) => { + const { t } = useTranslation(); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const tokenLimit = useMemo(() => { - let maxTokens = 13000; + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - nodeList.forEach((item) => { - if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) { - const model = - item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || ''; - const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000; + const { referenceList, formatValue } = useReference({ + nodeId, + valueType: inputChildren.valueType, + value: inputChildren.value + }); - maxTokens = Math.max(maxTokens, quoteMaxToken); - } - }); + const onSelect = useCallback( + (e: ReferenceValueProps) => { + const workflowStartNode = nodeList.find( + (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart + ); + + onChangeNode({ + nodeId, + type: 'replaceInput', + key: inputChildren.key, + value: { + ...inputChildren, + value: + e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) + ? [VARIABLE_NODE_ID, e[1]] + : e + } + }); + }, + [inputChildren, nodeId, nodeList, onChangeNode] + ); - return maxTokens; - }, [nodeList, llmModelList]); + const onDel = useCallback(() => { + onChangeNode({ + nodeId, + type: 'delInput', + key: inputChildren.key + }); + }, [inputChildren.key, nodeId, onChangeNode]); + + return ( + <> + + {t(inputChildren.label as any)} + {/* value */} + + + + + + + ); + } + ); const CustomComponent = useMemo(() => { + const quoteList = inputs.filter((item) => item.canEdit); + const tokenLimit = (() => { + let maxTokens = 13000; + + nodeList.forEach((item) => { + if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) { + const model = + item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || ''; + const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000; + + maxTokens = Math.max(maxTokens, quoteMaxToken); + } + }); + + return maxTokens; + })(); + return { [NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => ( @@ -115,98 +192,23 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps) => { ); } }; - }, [nodeId, onChangeNode, quoteList, t, tokenLimit]); - - return ( - - - - {/* {RenderQuoteList} */} - - - - - - - ); + }, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]); + + const Render = useMemo(() => { + return ( + + + + {/* {RenderQuoteList} */} + + + + + + + ); + }, [CustomComponent, data, inputs, nodeId, outputs, selected, t]); + + return Render; }; export default React.memo(NodeDatasetConcat); - -function Reference({ - nodeId, - inputChildren -}: { - nodeId: string; - inputChildren: FlowNodeInputItemType; -}) { - const { t } = useTranslation(); - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - - const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - - const { referenceList, formatValue } = useReference({ - nodeId, - valueType: inputChildren.valueType, - value: inputChildren.value - }); - - const onSelect = useCallback( - (e: ReferenceValueProps) => { - const workflowStartNode = nodeList.find( - (node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart - ); - - onChangeNode({ - nodeId, - type: 'replaceInput', - key: inputChildren.key, - value: { - ...inputChildren, - value: - e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1]) - ? [VARIABLE_NODE_ID, e[1]] - : e - } - }); - }, - [inputChildren, nodeId, nodeList, onChangeNode] - ); - - const onDel = useCallback(() => { - onChangeNode({ - nodeId, - type: 'delInput', - key: inputChildren.key - }); - }, [inputChildren.key, nodeId, onChangeNode]); - - return ( - <> - - {t(inputChildren.label as any)} - {/* value */} - - - - - - - ); -} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLaf.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLaf.tsx index b8a3e907d64..4a0c8d6bbf5 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLaf.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLaf.tsx @@ -47,9 +47,10 @@ const NodeLaf = (props: NodeProps) => { const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - const requestUrl = inputs.find( - (item) => item.key === NodeInputKeyEnum.httpReqUrl - ) as FlowNodeInputItemType; + const requestUrl = useMemo( + () => inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl) as FlowNodeInputItemType, + [inputs] + ); const { userInfo, initUserInfo } = useUserStore(); @@ -217,63 +218,90 @@ const NodeLaf = (props: NodeProps) => { } ); - // not config laf - if (!token || !appid) { - return ( - - - - ); - } else { - return ( - - - {/* select function */} - { - onChangeNode({ - nodeId, - type: 'updateInput', - key: NodeInputKeyEnum.httpReqUrl, - value: { - ...requestUrl, - value: e - } - }); - }} - value={selectedFunction} - /> - {/* auto set params and go to edit */} - {!!selectedFunction && ( - - - + - - )} - - {!!selectedFunction && } - - ); - } + if (!lafFunction) return; + const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`; + window.open(url, '_blank'); + }} + > + {t('common:plugin.go to laf')} + + + )} + + {!!selectedFunction && } + + ); + } + }, [ + token, + appid, + selected, + data, + isLoadingFunctions, + lafFunctionSelectList, + t, + selectedFunction, + isSyncing, + onSyncParams, + props, + onChangeNode, + nodeId, + requestUrl, + lafData?.lafFunctions, + lafData?.lafApp?.appid, + feConfigs.lafEnv + ]); + + return Render; }; export default React.memo(NodeLaf); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx index 02051415099..a03b0f89d8a 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx @@ -414,7 +414,9 @@ const FieldEditModal = ({ {showValueTypeSelect ? ( - list={valueTypeSelectList} + list={valueTypeSelectList.filter( + (item) => item.value !== WorkflowIOValueTypeEnum.arrayAny + )} value={valueType} onchange={(e) => { setValue('valueType', e); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/VariableTable.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/VariableTable.tsx index ea1fddbe7cd..0bf15c29204 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/VariableTable.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/VariableTable.tsx @@ -2,7 +2,6 @@ import React from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import { useI18n } from '@/web/context/I18n'; const VariableTable = ({ variables = [], @@ -14,7 +13,6 @@ const VariableTable = ({ onDelete: (key: string) => void; }) => { const { t } = useTranslation(); - const { workflowT } = useI18n(); const showToolColumn = variables.some((item) => item.isTool); return ( @@ -27,7 +25,7 @@ const VariableTable = ({ {t('common:core.module.variable.variable name')} {t('common:core.workflow.Value type')} - {showToolColumn && {workflowT('tool_input')}} + {showToolColumn && {t('workflow:tool_input')}} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSimple.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSimple.tsx index f2fc8eb8907..0d9e6d66167 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSimple.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSimple.tsx @@ -21,39 +21,43 @@ const NodeSimple = ({ const { t } = useTranslation(); const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs); const { nodeId, inputs, outputs } = data; - const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); - const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]); + const Render = useMemo(() => { + const { isTool, commonInputs } = splitToolInputs(inputs, nodeId); + const filterHiddenInputs = commonInputs.filter((item) => true); - return ( - - {isTool && ( - <> - - - - - )} - {filterHiddenInputs.length > 0 && ( - <> - - - - - - )} - {outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && ( - <> - - - - - - )} - - ); + return ( + + {isTool && ( + <> + + + + + )} + {filterHiddenInputs.length > 0 && ( + <> + + + + + + )} + {outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && ( + <> + + + + + + )} + + ); + }, [splitToolInputs, inputs, nodeId, minW, maxW, selected, data, t, outputs]); + + return Render; }; export default React.memo(NodeSimple); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx index 331ecccde47..d2dc3458491 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, useMemo, useTransition } from 'react'; +import React, { Dispatch, useMemo } from 'react'; import { NodeProps } from 'reactflow'; import { Box } from '@chakra-ui/react'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx index a639d6232eb..f98720bf798 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx @@ -119,23 +119,49 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle }: { nodeId: string; }) { - const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); + const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx); - const { showHandle, LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => { + const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => { const node = nodeList.find((node) => node.nodeId === nodeId); const connectingNode = nodeList.find((node) => node.nodeId === connectingEdge?.nodeId); - const sourceEdges = edges.filter((edge) => edge.target === connectingNode?.nodeId); - const connectingNodeSourceNodeIds = sourceEdges.map((edge) => edge.source); + const connectingNodeSourceNodeIdMap = new Map(); + let forbidConnect = false; + edges.forEach((edge) => { + if (edge.target === connectingNode?.nodeId) { + connectingNodeSourceNodeIdMap.set(edge.source, 1); + } else if (edge.target === nodeId) { + // Node has be connected tool, it cannot be connect by other handle + if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) { + forbidConnect = true; + } + // The same source handle cannot connect to the same target node + if ( + connectingEdge && + connectingEdge.handleId === edge.sourceHandle && + edge.target === nodeId + ) { + forbidConnect = true; + } + } + }); const showHandle = (() => { + if (forbidConnect) return false; if (!node) return false; + + // Tool connecting + if (connectingEdge && connectingEdge.handleId === NodeOutputKeyEnum.selectedTools) + return false; + // Unable to connect oneself if (connectingEdge && connectingEdge.nodeId === nodeId) return false; + // Not the same parent node + if (connectingNode && connectingNode?.parentNodeId !== node?.parentNodeId) return false; + // Unable to connect to the source node - if (connectingNodeSourceNodeIds.includes(nodeId)) return false; + if (connectingNodeSourceNodeIdMap.has(nodeId)) return false; + return true; })(); @@ -150,6 +176,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle handleId={handleId} position={Position.Left} translate={[-2, 0]} + showHandle={showHandle} /> ); })(); @@ -164,6 +191,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle handleId={handleId} position={Position.Right} translate={[2, 0]} + showHandle={showHandle} /> ); })(); @@ -178,6 +206,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle handleId={handleId} position={Position.Top} translate={[0, -2]} + showHandle={showHandle} /> ); })(); @@ -192,6 +221,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle handleId={handleId} position={Position.Bottom} translate={[0, 2]} + showHandle={showHandle} /> ); })(); @@ -205,14 +235,14 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle }; }, [connectingEdge, edges, nodeId, nodeList]); - return showHandle ? ( + return ( <> {LeftHandle} {rightHandle} {topHandle} {bottomHandle} - ) : null; + ); }); export default function Dom() { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx index 406e828fc99..f70a8820d52 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx @@ -117,6 +117,7 @@ const MySourceHandle = React.memo(function MySourceHandle({ if (!node) return null; if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null; + return <>{RenderHandle}; }); @@ -136,18 +137,16 @@ const MyTargetHandle = React.memo(function MyTargetHandle({ position, translate, highlightStyle, - connectedStyle + connectedStyle, + showHandle }: Props & { + showHandle: boolean; highlightStyle: Record; connectedStyle: Record; }) { - const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); + const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx); - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]); const connected = edges.some((edge) => edge.targetHandle === handleId); - const connectedEdges = edges.filter((edge) => edge.target === nodeId); const translateStr = useMemo(() => { if (!translate) return ''; @@ -190,30 +189,6 @@ const MyTargetHandle = React.memo(function MyTargetHandle({ return; }, [connected, connectingEdge, connectedStyle, highlightStyle, transform]); - const showHandle = useMemo(() => { - if (!node) return false; - // check tool connected - if ( - edges.some( - (edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools - ) - ) { - return false; - } - - if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false; - - // From same source node and same handle - if ( - connectedEdges.some( - (item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId - ) - ) - return false; - - return true; - }, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]); - const RenderHandle = useMemo(() => { return ( { +export const TargetHandle = ( + props: Props & { + showHandle: boolean; + } +) => { return ( { intro, minW = '300px', maxW = '600px', + minH = 0, + w = 'full', + h = 'full', nodeId, selected, menuForbid, @@ -222,7 +228,7 @@ const NodeCard = (props: Props) => { )} - + @@ -234,11 +240,12 @@ const NodeCard = (props: Props) => { avatar, t, name, - menuForbid, hasNewVersion, onOpenConfirmSync, onClickSyncVersion, nodeTemplate?.diagram, + menuForbid, + nodeList, intro, ConfirmSyncModal, onOpenCustomTitleModal, @@ -255,13 +262,17 @@ const NodeCard = (props: Props) => { }, [nodeId]); return ( - { {RenderHandle} - + ); }; @@ -299,16 +310,17 @@ export default React.memo(NodeCard); const MenuRender = React.memo(function MenuRender({ nodeId, - menuForbid + menuForbid, + nodeList }: { nodeId: string; menuForbid?: Props['menuForbid']; + nodeList: FlowNodeItemType[]; }) { const { t } = useTranslation(); const { openDebugNode, DebugInputModal } = useDebug(); - const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes); - const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges); + const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v); const { computedNewNodeName } = useWorkflowUtils(); const onCopyNode = useCallback( @@ -347,6 +359,7 @@ const MenuRender = React.memo(function MenuRender({ version: template.version }, selected: true, + parentNodeId: undefined, t }) ); @@ -356,10 +369,26 @@ const MenuRender = React.memo(function MenuRender({ ); const onDelNode = useCallback( (nodeId: string) => { - setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId)); - setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)); + // Remove node and its child nodes + setNodes((state) => + state.filter((item) => item.data.nodeId !== nodeId && item.data.parentNodeId !== nodeId) + ); + + // Remove edges connected to the node and its child nodes + const childNodeIds = nodeList + .filter((node) => node.parentNodeId === nodeId) + .map((node) => node.nodeId); + setEdges((state) => + state.filter( + (edge) => + edge.source !== nodeId && + edge.target !== nodeId && + !childNodeIds.includes(edge.target) && + !childNodeIds.includes(edge.source) + ) + ); }, - [setEdges, setNodes] + [nodeList, setEdges, setNodes] ); const Render = useMemo(() => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/FieldEditModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/FieldEditModal.tsx index 303cdf024f8..8e2ff1a10fb 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/FieldEditModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/FieldEditModal.tsx @@ -1,4 +1,3 @@ -import { useI18n } from '@/web/context/I18n'; import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant'; import { Box, @@ -39,7 +38,6 @@ const FieldModal = ({ onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void; }) => { const { t } = useTranslation(); - const { workflowT, commonT } = useI18n(); const { toast } = useToast(); const isEdit = !!defaultInput.key; @@ -56,7 +54,7 @@ const FieldModal = ({ return false; }, [customInputConfig.selectValueTypeList, inputType]); - const valueTypeSelectLit = useMemo(() => { + const valueTypeSelectList = useMemo(() => { if (!customInputConfig.selectValueTypeList) return []; const dataTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({ @@ -88,7 +86,7 @@ const FieldModal = ({ if (!isEdit || isChangeKey) { toast({ status: 'warning', - title: workflowT('field_name_already_exists') + title: t('workflow:field_name_already_exists') }); return; } @@ -103,7 +101,7 @@ const FieldModal = ({ }); onClose(); }, - [defaultInput.key, isEdit, keys, onClose, onSubmit, toast, workflowT] + [defaultInput.key, isEdit, keys, onClose, onSubmit, toast, t] ); const onSubmitError = useCallback( (e: Object) => { @@ -124,18 +122,20 @@ const FieldModal = ({ {showValueTypeSelect && ( - {commonT('core.module.Data Type')} + {t('common:core.module.Data Type')} w={'full'} - list={valueTypeSelectLit} + list={valueTypeSelectList.filter( + (item) => item.value !== WorkflowIOValueTypeEnum.arrayAny + )} value={valueType} onchange={(e) => { setValue('valueType', e); @@ -159,7 +159,7 @@ const FieldModal = ({ {customInputConfig.showDescription && ( - {workflowT('input_description')} + {t('workflow:input_description')}