From f27cf83e073acc203284f9138931c7e2153a0058 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Tue, 10 Sep 2024 15:28:00 +0800 Subject: [PATCH 1/7] loop node frontend --- packages/global/core/workflow/constants.ts | 13 +- .../global/core/workflow/node/constant.ts | 9 +- .../core/workflow/template/constants.ts | 10 +- .../core/workflow/template/system/loop.ts | 55 ++++++ .../core/workflow/template/system/loopEnd.ts | 34 ++++ .../workflow/template/system/loopStart.ts | 21 +++ packages/global/core/workflow/type/node.d.ts | 2 + .../web/components/common/EmptyTip/index.tsx | 6 +- .../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 ++ packages/web/i18n/en/workflow.json | 3 + packages/web/i18n/zh/workflow.json | 10 ++ .../Flow/NodeTemplatesModal.tsx | 48 ++++-- .../Flow/components/Container.tsx | 7 +- .../Flow/components/IOTitle.tsx | 2 +- .../Flow/hooks/useWorkflow.tsx | 160 +++++++++++++++++- .../WorkflowComponents/Flow/index.tsx | 9 +- .../Flow/nodes/NodeLoop.tsx | 66 ++++++++ .../Flow/nodes/NodeLoopEnd.tsx | 32 ++++ .../Flow/nodes/NodeLoopStart.tsx | 109 ++++++++++++ .../Flow/nodes/NodePluginIO/VariableTable.tsx | 53 +++--- .../Flow/nodes/render/NodeCard.tsx | 98 ++++++++--- .../Flow/nodes/render/RenderInput/Label.tsx | 11 +- .../RenderInput/templates/Reference.tsx | 5 +- projects/app/src/web/core/workflow/utils.ts | 21 ++- 28 files changed, 739 insertions(+), 87 deletions(-) create mode 100644 packages/global/core/workflow/template/system/loop.ts create mode 100644 packages/global/core/workflow/template/system/loopEnd.ts create mode 100644 packages/global/core/workflow/template/system/loopStart.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/NodeLoop.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index eab81402362c..76ec37701044 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,12 @@ export enum NodeInputKeyEnum { fileUrlList = 'fileUrlList', // user select - userSelectOptions = 'userSelectOptions' + userSelectOptions = 'userSelectOptions', + + // loop + loopInputArray = 'loopInputArray', + loopFlow = 'loopFlow', + loopOutputArray = 'loopOutputArray' } export enum NodeOutputKeyEnum { @@ -178,7 +184,10 @@ export enum NodeOutputKeyEnum { ifElseResult = 'ifElseResult', //user select - selectResult = 'selectResult' + selectResult = 'selectResult', + + // loop + loopArray = 'loopArray' } export enum VariableInputEnum { diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index e34813f1b5e5..34e2a6f12178 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/template/constants.ts b/packages/global/core/workflow/template/constants.ts index 2d2dcb038fc4..0b583dfb2518 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'; +import { LoopStartNode } from './system/loopStart'; +import { LoopEndNode } from './system/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/system/loop.ts b/packages/global/core/workflow/template/system/loop.ts new file mode 100644 index 000000000000..ad986b19e2ef --- /dev/null +++ b/packages/global/core/workflow/template/system/loop.ts @@ -0,0 +1,55 @@ +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + FlowNodeTypeEnum +} from '../../node/constant'; +import { FlowNodeTemplateType } from '../../type/node.d'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../constants'; +import { getHandleConfig } from '../utils'; +import { i18nT } from '../../../../../web/i18n/utils'; + +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') + }, + { + key: NodeInputKeyEnum.loopFlow, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + value: { + width: 0, + height: 0 + } + } + ], + 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/loopEnd.ts b/packages/global/core/workflow/template/system/loopEnd.ts new file mode 100644 index 000000000000..e602ebd466de --- /dev/null +++ b/packages/global/core/workflow/template/system/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.loopOutputArray, + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayAny, + label: '', + required: true, + value: [] + } + ], + outputs: [] +}; diff --git a/packages/global/core/workflow/template/system/loopStart.ts b/packages/global/core/workflow/template/system/loopStart.ts new file mode 100644 index 000000000000..39a95ebded9f --- /dev/null +++ b/packages/global/core/workflow/template/system/loopStart.ts @@ -0,0 +1,21 @@ +import { FlowNodeTypeEnum } from '../../node/constant'; +import { FlowNodeTemplateType } from '../../type/node.d'; +import { FlowNodeTemplateTypeEnum } 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: [], + outputs: [] +}; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 26fdb8681945..22be80881779 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'; @@ -103,6 +104,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & { response?: ChatHistoryItemResType; isExpired?: boolean; }; + isIntersecting?: boolean; }; // store node type diff --git a/packages/web/components/common/EmptyTip/index.tsx b/packages/web/components/common/EmptyTip/index.tsx index 80b069558264..5c8161a6f9cd 100644 --- a/packages/web/components/common/EmptyTip/index.tsx +++ b/packages/web/components/common/EmptyTip/index.tsx @@ -5,13 +5,15 @@ import { useTranslation } from 'next-i18next'; type Props = FlexProps & { text?: string | React.ReactNode; + iconW?: string | number; + iconH?: string | number; }; -const EmptyTip = ({ text, ...props }: Props) => { +const EmptyTip = ({ text, iconW, iconH, ...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 d3ee88a17f5f..00fd25f5a93d 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 000000000000..9e3bf1b2df71 --- /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 000000000000..548dba9a7b6e --- /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 000000000000..ca8fc3c91455 --- /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 000000000000..546a3ecd5dfc --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index c3c166f8e238..671865567584 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,7 @@ "length_not_equal_to": "Length Not Equal To", "less_than": "Less Than", "less_than_or_equal_to": "Less Than or Equal To", + "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/workflow.json b/packages/web/i18n/zh/workflow.json index f67c152c2acc..11ae86d93af2 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/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx index e478859ec7f8..dcc7e3aa7ce2 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 @@ -14,7 +14,7 @@ import type { NodeTemplateListItemType, NodeTemplateListType } from '@fastgpt/global/core/workflow/type/node.d'; -import { useViewport, XYPosition } from 'reactflow'; +import { useReactFlow, useViewport, 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'; @@ -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,37 @@ 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, + zIndex: templateNode.flowNodeType === FlowNodeTypeEnum.loop ? -1001 : 0 }); + const newNodes = [newNode]; + + if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) { + const loopStartNode = moduleTemplatesFlat.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.loopStart + ); + const loopEndNode = moduleTemplatesFlat.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.loopEnd + ); + + if (!loopStartNode || !loopEndNode) { + return; + } + + const startNode = nodeTemplate2FlowNode({ + template: loopStartNode, + position: { x: mouseX + 60, y: mouseY + 280 }, + parentNodeId: newNode.id + }); + const endNode = nodeTemplate2FlowNode({ + template: { ...loopEndNode }, + position: { x: mouseX + 420, y: mouseY + 680 }, + parentNodeId: newNode.id + }); + + newNodes.push(startNode, endNode); + } setNodes((state) => { const newState = state @@ -493,11 +521,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/Container.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/Container.tsx index 47279ca3b430..90b38e6dbf54 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 a65b5351a9f2..475f10259c47 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/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index a5bf321e7e65..d6f8874f7e1d 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,16 @@ import { Edge, Node, NodePositionChange, - XYPosition + XYPosition, + useReactFlow, + getNodesBounds, + Rect } from 'reactflow'; -import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant'; +import { + EDGE_TYPE, + FlowNodeInputTypeEnum, + 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,6 +25,7 @@ import { useKeyboard } from './useKeyboard'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; +import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; /* Compute helper lines for snapping nodes to each other @@ -259,8 +267,49 @@ 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, + setNodes, + onChangeNode, + onEdgesChange, + setHoverEdgeId + } = useContextSelector(WorkflowContext, (v) => v); + const { getIntersectingNodes } = useReactFlow(); + + const resetNodeSizeAndPosition = (rect: Rect, parentId: string) => { + onChangeNode({ + nodeId: parentId || '', + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: { + key: NodeInputKeyEnum.loopFlow, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + value: { + width: rect.width + 110 > 900 ? rect.width + 110 : 900, + height: rect.height + 380 > 900 ? rect.height + 380 : 900 + } + } + }); + setNodes((nodes) => { + return nodes.map((node) => { + if (node.id === parentId) { + return { + ...node, + position: { + x: rect.x - 50, + y: rect.y - 280 + } + }; + } + return node; + }); + }); + }; /* helper line */ const [helperLineHorizontal, setHelperLineHorizontal] = useState(); @@ -315,6 +364,20 @@ export const useWorkflow = () => { status: 'warning', title: t('common:core.workflow.Can not delete node') }); + } else if (node && nodes.find((item) => item.data.parentNodeId === node.id)) { + const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id); + const childNodesChange = childNodes.map((node) => { + return { + ...change, + id: node.id + }; + }); + return (() => { + onNodesChange(changes.concat(childNodesChange)); + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + })(); } else { return (() => { onNodesChange(changes); @@ -325,6 +388,55 @@ export const useWorkflow = () => { } } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { change.selected = true; + } else if (change.type === 'position' && change.position) { + const node = nodes.find((n) => n.id === change.id); + if (node && node.data.parentNodeId) { + const parentId = node.data.parentNodeId; + const parentNode = nodes.find((n) => n.id === parentId); + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); + + if (!parentNode) return; + const rect = getNodesBounds(childNodes); + return (() => { + customApplyNodeChanges(changes, childNodes); + onNodesChange(changes); + resetNodeSizeAndPosition(rect, parentId); + })(); + } else if (node && nodes.find((item) => item.data.parentNodeId === node.id)) { + const parentId = node.id; + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); + const initPosition = node.position; + const deltaX = change.position.x - initPosition.x; + const deltaY = change.position.y - initPosition.y; + const childNodesChange = childNodes.map((node) => { + if (change.dragging) { + return { + ...change, + id: node.id, + position: { + x: node.position.x + deltaX, + y: node.position.y + deltaY + }, + positionAbsolute: { + x: node.position.x + deltaX, + y: node.position.y + deltaY + } + }; + } else { + return { + ...change, + id: node.id + }; + } + }); + return (() => { + // customApplyNodeChanges( + // changes, + // nodes.filter((node) => !node.data.parentNodeId) + // ); + onNodesChange(changes.concat(childNodesChange)); + })(); + } } } @@ -338,6 +450,42 @@ export const useWorkflow = () => { }, [onEdgesChange] ); + // const onNodeDrag = useCallback((_: any, node: Node) => {}, []); + + const onNodeDragStop = useCallback((_: any, node: Node) => { + const intersections = getIntersectingNodes(node); + const parentNode = intersections.find((item) => item.type === '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).concat(node); + const rect = getNodesBounds(childNodes); + resetNodeSizeAndPosition(rect, parentNode.id); + } + }, []); /* connect */ const onConnectStart = useCallback( @@ -403,7 +551,9 @@ export const useWorkflow = () => { onEdgeMouseEnter, onEdgeMouseLeave, helperLineHorizontal, - helperLineVertical + helperLineVertical, + // onNodeDrag, + 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 455ee74e37fa..b18f54939fdb 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/NodeLoop')), + [FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/NodeLoopStart')), + [FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/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/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx new file mode 100644 index 000000000000..301f683cea65 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx @@ -0,0 +1,66 @@ +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import React, { useCallback } from 'react'; +import { Background, NodeProps, NodeResizeControl, NodeResizer, OnResize } 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 MyIcon from '@fastgpt/web/components/common/Icon'; +import { Box, Center, Flex } 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'; + +const NodeLoop = ({ data, selected, zIndex }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId, inputs, outputs, isIntersecting } = data; + + const loopFlowData = inputs.find((input) => input.key === NodeInputKeyEnum.loopFlow); + + return ( + + + + + + + + {t('workflow:loop_body')} + + + + + {/* + } + /> */} + + + + + + ); +}; + +export default React.memo(NodeLoop); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx new file mode 100644 index 000000000000..d3119d578cac --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx @@ -0,0 +1,32 @@ +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 Reference from './render/RenderInput/templates/Reference'; +import { Box } from '@chakra-ui/react'; + +const NodeLoopEnd = ({ data, selected }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId, inputs, outputs } = data; + const inputItem = inputs.find((input) => input.key === 'loopOutputArray'); + + return ( + + + {inputItem && } + + + ); +}; + +export default NodeLoopEnd; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx new file mode 100644 index 000000000000..70bf9888a282 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx @@ -0,0 +1,109 @@ +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 VariableTable from './NodePluginIO/VariableTable'; +import { Box } from '@chakra-ui/react'; +import { useEffect } from 'react'; +import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; + +const typeMap = { + [WorkflowIOValueTypeEnum.arrayString]: 'String', + [WorkflowIOValueTypeEnum.arrayNumber]: 'Number', + [WorkflowIOValueTypeEnum.arrayBoolean]: 'Boolean', + [WorkflowIOValueTypeEnum.arrayObject]: 'Object', + [WorkflowIOValueTypeEnum.arrayAny]: 'Any' +}; + +const NodeLoopStart = ({ data, selected }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId } = data; + const { nodes, nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); + const loopStartNode = nodes.find((node) => node.id === nodeId); + const parentNode = nodes.find((node) => node.id === loopStartNode?.data.parentNodeId); + const arrayInput = parentNode?.data.inputs.find( + (input) => input.key === NodeInputKeyEnum.loopInputArray + ); + + const outputValueType = !!arrayInput?.value + ? nodeList + .find((node) => node.nodeId === arrayInput?.value[0]) + ?.outputs.find((output) => output.id === arrayInput?.value[1])?.valueType + : undefined; + + useEffect(() => { + if ( + !outputValueType && + loopStartNode?.data.outputs.find((output) => output.key === NodeOutputKeyEnum.loopArray) + ) { + onChangeNode({ + nodeId, + type: 'delOutput', + key: NodeOutputKeyEnum.loopArray + }); + } else if ( + outputValueType && + !loopStartNode?.data.outputs.find((output) => output.key === NodeOutputKeyEnum.loopArray) + ) { + onChangeNode({ + nodeId, + type: 'addOutput', + value: { + id: NodeOutputKeyEnum.loopArray, + key: NodeOutputKeyEnum.loopArray, + label: t('workflow:Array_element'), + type: FlowNodeOutputTypeEnum.static, + valueType: WorkflowIOValueTypeEnum.arrayAny + } + }); + } + }, [onChangeNode, outputValueType]); + + return ( + + + {!outputValueType ? ( + + ) : ( + + )} + + + ); +}; + +export default NodeLoopStart; 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 ea1fddbe7cd9..2e6657d1d340 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 @@ -10,11 +10,10 @@ const VariableTable = ({ onDelete }: { variables: { icon?: string; label: string; type: string; key: string; isTool?: boolean }[]; - onEdit: (key: string) => void; - onDelete: (key: string) => void; + onEdit?: (key: string) => void; + onDelete?: (key: string) => void; }) => { const { t } = useTranslation(); - const { workflowT } = useI18n(); const showToolColumn = variables.some((item) => item.isTool); return ( @@ -27,7 +26,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')}} @@ -39,32 +38,38 @@ const VariableTable = ({ {!!item.icon && ( )} + {/* */} {item.label || item.key} + {/* */} {item.type} {showToolColumn && {item.isTool ? '✅' : '-'}} - onEdit(item.key)} - /> - { - onDelete(item.key); - }} - /> + {onEdit && ( + onEdit(item.key)} + /> + )} + {onDelete && ( + { + onDelete(item.key); + }} + /> + )} ))} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 98e0a934dd6a..fb070bfab193 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -30,6 +30,9 @@ type Props = FlowNodeItemType & { children?: React.ReactNode | React.ReactNode[] | string; minW?: string | number; maxW?: string | number; + minH?: string | number; + w?: string | number; + h?: string | number; selected?: boolean; menuForbid?: { debug?: boolean; @@ -50,6 +53,9 @@ const NodeCard = (props: Props) => { intro, minW = '300px', maxW = '600px', + minH = 0, + w, + h, nodeId, selected, menuForbid, @@ -255,13 +261,17 @@ const NodeCard = (props: Props) => { }, [nodeId]); return ( - { {RenderHandle} - + ); }; @@ -331,32 +341,76 @@ const MenuRender = React.memo(function MenuRender({ pluginId: node.data.pluginId, version: node.data.version }; - return state.concat( - storeNode2FlowNode({ - item: { - flowNodeType: template.flowNodeType, - avatar: template.avatar, - name: template.name, - intro: template.intro, - nodeId: getNanoid(), - position: { x: node.position.x + 200, y: node.position.y + 50 }, - showStatus: template.showStatus, - pluginId: template.pluginId, - inputs: template.inputs, - outputs: template.outputs, - version: template.version - }, - selected: true, - t - }) - ); + const childNodes = state.filter((item) => item.data.parentNodeId === nodeId); + + const childNodeTemplates = childNodes.map((item) => ({ + avatar: item.data.avatar, + name: computedNewNodeName({ + templateName: item.data.name, + flowNodeType: item.data.flowNodeType, + pluginId: item.data.pluginId + }), + intro: item.data.intro, + flowNodeType: item.data.flowNodeType, + inputs: item.data.inputs, + outputs: item.data.outputs, + showStatus: item.data.showStatus, + pluginId: item.data.pluginId, + version: item.data.version, + position: { x: item.position.x + 200, y: item.position.y + 50 } + })); + const currentNodeId = getNanoid(); + return state + .concat( + storeNode2FlowNode({ + item: { + flowNodeType: template.flowNodeType, + avatar: template.avatar, + name: template.name, + intro: template.intro, + nodeId: currentNodeId, + position: { x: node.position.x + 200, y: node.position.y + 50 }, + showStatus: template.showStatus, + pluginId: template.pluginId, + inputs: template.inputs, + outputs: template.outputs, + version: template.version + }, + selected: true, + zIndex: childNodes.length > 0 ? -1001 : 0, + t + }) + ) + .concat( + childNodeTemplates.map((template) => + storeNode2FlowNode({ + item: { + flowNodeType: template.flowNodeType, + avatar: template.avatar, + name: template.name, + intro: template.intro, + nodeId: getNanoid(), + position: template.position, + showStatus: template.showStatus, + pluginId: template.pluginId, + inputs: template.inputs, + outputs: template.outputs, + version: template.version + }, + parentNodeId: currentNodeId, + t + }) + ) + ); }); }, [computedNewNodeName, setNodes, t] ); const onDelNode = useCallback( (nodeId: string) => { - setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId)); + setNodes((state) => + state.filter((item) => item.data.nodeId !== nodeId && item.data.parentNodeId !== nodeId) + ); setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)); }, [setEdges, setNodes] diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx index 60cc135d2714..cf6f4351290f 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx @@ -48,13 +48,10 @@ const InputLabel = ({ nodeId, input }: Props) => { return ( - - {t(label as any)} + + + {t(label as any)} + {description && } {/* value type */} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 5ae2f3044544..ca11b22756e8 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -130,7 +130,10 @@ export const useReference = ({ (output) => valueType === WorkflowIOValueTypeEnum.any || output.valueType === WorkflowIOValueTypeEnum.any || - output.valueType === valueType + output.valueType === valueType || + // When valueType is arrayAny, return all array type outputs + (valueType === WorkflowIOValueTypeEnum.arrayAny && + output.valueType?.includes('array')) ) .filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam) .map((output) => { diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 5434bf3e793a..46241e6ed326 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -40,16 +40,21 @@ import { workflowSystemVariables } from '../app/utils'; export const nodeTemplate2FlowNode = ({ template, position, - selected + selected, + parentNodeId, + zIndex }: { template: FlowNodeTemplateType; position: XYPosition; selected?: boolean; + parentNodeId?: string; + zIndex?: number; }): Node => { // replace item data const moduleItem: FlowNodeItemType = { ...template, - nodeId: getNanoid() + nodeId: getNanoid(), + parentNodeId }; return { @@ -57,16 +62,21 @@ export const nodeTemplate2FlowNode = ({ type: moduleItem.flowNodeType, data: moduleItem, position: position, - selected + selected, + zIndex }; }; export const storeNode2FlowNode = ({ item: storeNode, selected = false, + zIndex, + parentNodeId, t }: { item: StoreNodeItemType; selected?: boolean; + zIndex?: number; + parentNodeId?: string; t: TFunction; }): Node => { // init some static data @@ -88,7 +98,7 @@ export const storeNode2FlowNode = ({ ...storeNode, avatar: template.avatar ?? storeNode.avatar, version: storeNode.version ?? template.version ?? defaultNodeVersion, - + parentNodeId, /* Inputs and outputs, New fields are added, not reduced */ @@ -150,7 +160,8 @@ export const storeNode2FlowNode = ({ type: storeNode.flowNodeType, data: nodeItem, selected, - position: storeNode.position || { x: 0, y: 0 } + position: storeNode.position || { x: 0, y: 0 }, + zIndex }; }; export const storeEdgesRenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => { From e6aa4aa6fdea2fdec06fd2592d5e4d71adfdf702 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Wed, 11 Sep 2024 16:25:55 +0800 Subject: [PATCH 2/7] loop-node --- packages/global/core/workflow/constants.ts | 12 +- .../global/core/workflow/runtime/type.d.ts | 10 ++ .../core/workflow/template/system/loop.ts | 6 +- .../core/workflow/template/system/loopEnd.ts | 4 +- .../workflow/template/system/loopStart.ts | 19 ++- .../service/core/workflow/dispatch/index.ts | 6 + .../core/workflow/dispatch/tools/runLoop.ts | 67 ++++++++++ .../workflow/dispatch/tools/runLoopEnd.ts | 21 +++ .../workflow/dispatch/tools/runLoopStart.ts | 24 ++++ packages/web/i18n/en/common.json | 8 +- packages/web/i18n/zh/common.json | 8 +- .../chat/components/WholeResponseModal.tsx | 24 +++- .../Flow/NodeTemplatesModal.tsx | 11 +- .../Flow/hooks/useWorkflow.tsx | 126 ++++++++++++------ .../Flow/nodes/NodeLoop.tsx | 43 +++--- .../Flow/nodes/NodeLoopEnd.tsx | 4 +- .../Flow/nodes/NodeLoopStart.tsx | 26 ++-- projects/app/src/web/core/workflow/utils.ts | 5 +- 18 files changed, 327 insertions(+), 97 deletions(-) create mode 100644 packages/service/core/workflow/dispatch/tools/runLoop.ts create mode 100644 packages/service/core/workflow/dispatch/tools/runLoopEnd.ts create mode 100644 packages/service/core/workflow/dispatch/tools/runLoopStart.ts diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 76ec37701044..f815bfa1e9c4 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -141,7 +141,12 @@ export enum NodeInputKeyEnum { // loop loopInputArray = 'loopInputArray', loopFlow = 'loopFlow', - loopOutputArray = 'loopOutputArray' + + // loop start + loopArrayElement = 'loopArrayElement', + + // loop end + loopOutputArrayElement = 'loopOutputArrayElement' } export enum NodeOutputKeyEnum { @@ -187,7 +192,10 @@ export enum NodeOutputKeyEnum { selectResult = 'selectResult', // loop - loopArray = 'loopArray' + loopArray = 'loopArray', + + // loop start + loopArrayElement = 'loopArrayElement' } export enum VariableInputEnum { diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 2339fd3ef0c3..a09bab79daf0 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -172,6 +172,16 @@ export type DispatchNodeResponseType = { // update var updateVarResult?: any[]; + + // loop start + loopInputElement?: any; + // loop end + loopOutputElement?: any; + + // loop + loopResult?: any[]; + loopInput?: any[]; + loopDetail?: ChatHistoryItemResType[]; }; export type DispatchNodeResultType = { diff --git a/packages/global/core/workflow/template/system/loop.ts b/packages/global/core/workflow/template/system/loop.ts index ad986b19e2ef..1293335543f8 100644 --- a/packages/global/core/workflow/template/system/loop.ts +++ b/packages/global/core/workflow/template/system/loop.ts @@ -30,7 +30,8 @@ export const LoopNode: FlowNodeTemplateType = { renderTypeList: [FlowNodeInputTypeEnum.reference], valueType: WorkflowIOValueTypeEnum.arrayAny, required: true, - label: i18nT('workflow:loop_input_array') + label: i18nT('workflow:loop_input_array'), + value: [] }, { key: NodeInputKeyEnum.loopFlow, @@ -39,7 +40,8 @@ export const LoopNode: FlowNodeTemplateType = { label: '', value: { width: 0, - height: 0 + height: 0, + childNodes: [] } } ], diff --git a/packages/global/core/workflow/template/system/loopEnd.ts b/packages/global/core/workflow/template/system/loopEnd.ts index e602ebd466de..cf32b9d19e84 100644 --- a/packages/global/core/workflow/template/system/loopEnd.ts +++ b/packages/global/core/workflow/template/system/loopEnd.ts @@ -22,9 +22,9 @@ export const LoopEndNode: FlowNodeTemplateType = { version: '4811', inputs: [ { - key: NodeInputKeyEnum.loopOutputArray, + key: NodeInputKeyEnum.loopOutputArrayElement, renderTypeList: [FlowNodeInputTypeEnum.reference], - valueType: WorkflowIOValueTypeEnum.arrayAny, + valueType: WorkflowIOValueTypeEnum.any, label: '', required: true, value: [] diff --git a/packages/global/core/workflow/template/system/loopStart.ts b/packages/global/core/workflow/template/system/loopStart.ts index 39a95ebded9f..405bf94eb542 100644 --- a/packages/global/core/workflow/template/system/loopStart.ts +++ b/packages/global/core/workflow/template/system/loopStart.ts @@ -1,6 +1,10 @@ -import { FlowNodeTypeEnum } from '../../node/constant'; +import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../node/constant'; import { FlowNodeTemplateType } from '../../type/node.d'; -import { FlowNodeTemplateTypeEnum } from '../../constants'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../constants'; import { getHandleConfig } from '../utils'; import { i18nT } from '../../../../../web/i18n/utils'; @@ -16,6 +20,15 @@ export const LoopStartNode: FlowNodeTemplateType = { forbidDelete: true, showStatus: false, version: '4811', - inputs: [], + inputs: [ + { + key: NodeInputKeyEnum.loopArrayElement, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.any, + label: '', + required: true, + value: '' + } + ], outputs: [] }; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 7ab7a9414602..8cb412a4ceaf 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -66,6 +66,9 @@ import { UserSelectInteractive } from '@fastgpt/global/core/workflow/template/system/userSelect/type'; import { dispatchRunAppNode } from './plugin/runApp'; +import { dispatchLoop } from './tools/runLoop'; +import { dispatchLoopEnd } from './tools/runLoopEnd'; +import { dispatchLoopStart } from './tools/runLoopStart'; const callbackMap: Record = { [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, diff --git a/packages/service/core/workflow/dispatch/tools/runLoop.ts b/packages/service/core/workflow/dispatch/tools/runLoop.ts new file mode 100644 index 000000000000..24718865fa38 --- /dev/null +++ b/packages/service/core/workflow/dispatch/tools/runLoop.ts @@ -0,0 +1,67 @@ +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { + DispatchNodeResultType, + ModuleDispatchProps +} from '@fastgpt/global/core/workflow/runtime/type'; +import { dispatchWorkFlow } from '..'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.loopInputArray]: Array; + [NodeInputKeyEnum.loopFlow]: { childNodes: Array }; +}>; +type Response = DispatchNodeResultType<{ + [NodeOutputKeyEnum.loopArray]: any[]; +}>; + +export const dispatchLoop = async (props: Props): Promise => { + const { params, runtimeNodes } = props; + const { + loopInputArray, + loopFlow: { childNodes } + } = params; + const runNodes = runtimeNodes.filter((node) => childNodes.includes(node.nodeId)); + const outputArray = []; + const loopDetail: ChatHistoryItemResType[] = []; + + for (const element 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.loopArrayElement + ? { + ...input, + value: element + } + : input + ) + } + : { + ...node, + isEntry: false + } + ) + }); + const loopOutputElement = response.flowResponses.find( + (res) => res.moduleType === FlowNodeTypeEnum.loopEnd + )?.loopOutputElement; + outputArray.push(loopOutputElement); + loopDetail.push(...response.flowResponses); + } + + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + loopInput: loopInputArray, + loopResult: outputArray, + loopDetail: loopDetail + }, + [NodeOutputKeyEnum.loopArray]: outputArray + }; +}; diff --git a/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts b/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts new file mode 100644 index 000000000000..ddc2638a8eaa --- /dev/null +++ b/packages/service/core/workflow/dispatch/tools/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.loopOutputArrayElement]: any; +}>; +type Response = DispatchNodeResultType<{}>; + +export const dispatchLoopEnd = async (props: Props): Promise => { + const { params } = props; + + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + loopOutputElement: params.loopOutputArrayElement + } + }; +}; diff --git a/packages/service/core/workflow/dispatch/tools/runLoopStart.ts b/packages/service/core/workflow/dispatch/tools/runLoopStart.ts new file mode 100644 index 000000000000..938d38e4f183 --- /dev/null +++ b/packages/service/core/workflow/dispatch/tools/runLoopStart.ts @@ -0,0 +1,24 @@ +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.loopArrayElement]: any; +}>; +type Response = DispatchNodeResultType<{ + [NodeOutputKeyEnum.loopArrayElement]: any; +}>; + +export const dispatchLoopStart = async (props: Props): Promise => { + const { params } = props; + + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + loopInputElement: params.loopArrayElement + }, + [NodeOutputKeyEnum.loopArrayElement]: params.loopArrayElement + }; +}; diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index b437ecf07cef..3ad17d037085 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -548,7 +548,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": { @@ -1482,4 +1486,4 @@ "verification": "Verification", "xx_search_result": "{{key}} Search Results", "yes": "Yes" -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 3a6971032972..2579528fc0f0 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -548,7 +548,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": { @@ -1482,4 +1486,4 @@ "verification": "验证", "xx_search_result": "{{key}} 的搜索结果", "yes": "是" -} \ No newline at end of file +} diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index b5c8a19c9619..848cb010fdd8 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'; @@ -40,6 +39,7 @@ export const WholeResponseContent = ({ showDetail: boolean; }) => { const { t } = useTranslation(); + console.log('activeModule', activeModule); // Auto scroll to top const ContentRef = useRef(null); @@ -337,6 +337,22 @@ export const WholeResponseContent = ({ label={t('common:core.chat.response.update_var_result')} value={activeModule?.updateVarResult} /> + + {/* loop */} + + + + {/* loopStart */} + + + {/* loopEnd */} + ) : null; }; @@ -525,6 +541,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 +571,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 { 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 dcc7e3aa7ce2..e50cbdeebb41 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 @@ -484,7 +484,8 @@ const RenderList = React.memo(function RenderList({ }, position: { x: mouseX, y: mouseY }, selected: true, - zIndex: templateNode.flowNodeType === FlowNodeTypeEnum.loop ? -1001 : 0 + zIndex: templateNode.flowNodeType === FlowNodeTypeEnum.loop ? -1001 : 0, + t }); const newNodes = [newNode]; @@ -503,12 +504,14 @@ const RenderList = React.memo(function RenderList({ const startNode = nodeTemplate2FlowNode({ template: loopStartNode, position: { x: mouseX + 60, y: mouseY + 280 }, - parentNodeId: newNode.id + parentNodeId: newNode.id, + t }); const endNode = nodeTemplate2FlowNode({ - template: { ...loopEndNode }, + template: loopEndNode, position: { x: mouseX + 420, y: mouseY + 680 }, - parentNodeId: newNode.id + parentNodeId: newNode.id, + t }); newNodes.push(startNode, endNode); 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 d6f8874f7e1d..f121e1e9a0bf 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 @@ -13,11 +13,7 @@ import { getNodesBounds, Rect } from 'reactflow'; -import { - EDGE_TYPE, - FlowNodeInputTypeEnum, - FlowNodeTypeEnum -} 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'; @@ -26,6 +22,7 @@ import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; /* Compute helper lines for snapping nodes to each other @@ -280,16 +277,20 @@ export const useWorkflow = () => { const { getIntersectingNodes } = useReactFlow(); const resetNodeSizeAndPosition = (rect: Rect, parentId: string) => { + const parentNode = nodes.find((node) => node.id === parentId); + const loopFlow = parentNode?.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + + if (!loopFlow) return; onChangeNode({ nodeId: parentId || '', type: 'updateInput', key: NodeInputKeyEnum.loopFlow, value: { - key: NodeInputKeyEnum.loopFlow, - renderTypeList: [FlowNodeInputTypeEnum.hidden], - valueType: WorkflowIOValueTypeEnum.any, - label: '', + ...loopFlow, value: { + ...loopFlow?.value, width: rect.width + 110 > 900 ? rect.width + 110 : 900, height: rect.height + 380 > 900 ? rect.height + 380 : 900 } @@ -384,6 +385,30 @@ export const useWorkflow = () => { setEdges((state) => state.filter((edge) => edge.source !== change.id && edge.target !== change.id) ); + if (node?.data.parentNodeId) { + const parentId = node.data.parentNodeId; + const childNodes = nodes.filter( + (n) => n.data.parentNodeId === parentId && n.id !== node.id + ); + const parentNode = nodes.find((n) => n.id === parentId); + const rect = getNodesBounds(childNodes); + const updatedLoopFlow = parentNode?.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + if (updatedLoopFlow) { + updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.filter( + (nodeId: string) => nodeId !== node.id + ); + } + resetNodeSizeAndPosition(rect, parentId); + updatedLoopFlow && + onChangeNode({ + nodeId: parentId, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: updatedLoopFlow + }); + } })(); } } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { @@ -450,42 +475,56 @@ export const useWorkflow = () => { }, [onEdgesChange] ); - // const onNodeDrag = useCallback((_: any, node: Node) => {}, []); - - const onNodeDragStop = useCallback((_: any, node: Node) => { - const intersections = getIntersectingNodes(node); - const parentNode = intersections.find((item) => item.type === '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') + + const onNodeDragStop = useCallback( + (_: any, node: Node) => { + const intersections = getIntersectingNodes(node); + const parentNode = intersections.find((item) => item.type === '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') + }); + } + const updatedLoopFlow = parentNode.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + if (updatedLoopFlow) { + updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.concat(node.id); + } + onChangeNode({ + nodeId: node.id, + type: 'attr', + key: 'parentNodeId', + value: parentNode.id }); - } - onChangeNode({ - nodeId: node.id, - type: 'attr', - key: 'parentNodeId', - value: parentNode.id - }); - setEdges((state) => - state.filter((edge) => edge.source !== node.id && edge.target !== node.id) - ); + onChangeNode({ + nodeId: parentNode.id, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: updatedLoopFlow + }); + setEdges((state) => + state.filter((edge) => edge.source !== node.id && edge.target !== node.id) + ); - const childNodes = nodes.filter((n) => n.data.parentNodeId === parentNode.id).concat(node); - const rect = getNodesBounds(childNodes); - resetNodeSizeAndPosition(rect, parentNode.id); - } - }, []); + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentNode.id).concat(node); + const rect = getNodesBounds(childNodes); + resetNodeSizeAndPosition(rect, parentNode.id); + } + }, + [getIntersectingNodes, onChangeNode, setEdges, nodes, toast, t, resetNodeSizeAndPosition] + ); /* connect */ const onConnectStart = useCallback( @@ -552,7 +591,6 @@ export const useWorkflow = () => { onEdgeMouseLeave, helperLineHorizontal, helperLineVertical, - // onNodeDrag, onNodeDragStop }; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx index 301f683cea65..faccb8cb1d15 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx @@ -1,22 +1,41 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Background, NodeProps, NodeResizeControl, NodeResizer, OnResize } 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 MyIcon from '@fastgpt/web/components/common/Icon'; import { Box, Center, Flex } 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 { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '../../context'; -const NodeLoop = ({ data, selected, zIndex }: NodeProps) => { +const NodeLoop = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); - const { nodeId, inputs, outputs, isIntersecting } = data; + const { nodeId, inputs, outputs } = data; + const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); const loopFlowData = inputs.find((input) => input.key === NodeInputKeyEnum.loopFlow); + const childNodes = nodeList.filter((node) => node.parentNodeId === nodeId); + + useEffect(() => { + loopFlowData && + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: { + ...loopFlowData, + value: { + ...loopFlowData?.value, + childNodes: childNodes.map((node) => node.nodeId) + } + } + }); + }, []); return ( ) => { - {/* - } - /> */} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx index d3119d578cac..cdbe6fbdb4de 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx @@ -8,8 +8,8 @@ import { Box } from '@chakra-ui/react'; const NodeLoopEnd = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); - const { nodeId, inputs, outputs } = data; - const inputItem = inputs.find((input) => input.key === 'loopOutputArray'); + const { nodeId, inputs } = data; + const inputItem = inputs.find((input) => input.key === 'loopOutputArrayElement'); return ( ) => { @@ -42,26 +42,30 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { useEffect(() => { if ( !outputValueType && - loopStartNode?.data.outputs.find((output) => output.key === NodeOutputKeyEnum.loopArray) + loopStartNode?.data.outputs.find( + (output) => output.key === NodeOutputKeyEnum.loopArrayElement + ) ) { onChangeNode({ nodeId, type: 'delOutput', - key: NodeOutputKeyEnum.loopArray + key: NodeOutputKeyEnum.loopArrayElement }); } else if ( outputValueType && - !loopStartNode?.data.outputs.find((output) => output.key === NodeOutputKeyEnum.loopArray) + !loopStartNode?.data.outputs.find( + (output) => output.key === NodeOutputKeyEnum.loopArrayElement + ) ) { onChangeNode({ nodeId, type: 'addOutput', value: { - id: NodeOutputKeyEnum.loopArray, - key: NodeOutputKeyEnum.loopArray, + id: NodeOutputKeyEnum.loopArrayElement, + key: NodeOutputKeyEnum.loopArrayElement, label: t('workflow:Array_element'), type: FlowNodeOutputTypeEnum.static, - valueType: WorkflowIOValueTypeEnum.arrayAny + valueType: typeMap[outputValueType as keyof typeof typeMap] } }); } diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 46241e6ed326..b0c3db1d73ec 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -42,17 +42,20 @@ export const nodeTemplate2FlowNode = ({ position, selected, parentNodeId, - zIndex + zIndex, + t }: { template: FlowNodeTemplateType; position: XYPosition; selected?: boolean; parentNodeId?: string; zIndex?: number; + t: TFunction; }): Node => { // replace item data const moduleItem: FlowNodeItemType = { ...template, + name: t(template.name as any), nodeId: getNanoid(), parentNodeId }; From 7dadc4c9e46a6aa29814e5cb5758d3bfdd2c085d Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Wed, 11 Sep 2024 17:46:33 +0800 Subject: [PATCH 3/7] fix-code --- packages/global/core/workflow/type/node.d.ts | 1 - .../core/workflow/dispatch/tools/runLoop.ts | 2 +- .../Flow/NodeTemplatesModal.tsx | 6 +- .../Flow/hooks/useWorkflow.tsx | 268 ++++++++++-------- .../Flow/nodes/NodeLoopStart.tsx | 1 - 5 files changed, 150 insertions(+), 128 deletions(-) diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 22be80881779..a5f121418ca2 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -104,7 +104,6 @@ export type FlowNodeItemType = FlowNodeTemplateType & { response?: ChatHistoryItemResType; isExpired?: boolean; }; - isIntersecting?: boolean; }; // store node type diff --git a/packages/service/core/workflow/dispatch/tools/runLoop.ts b/packages/service/core/workflow/dispatch/tools/runLoop.ts index 24718865fa38..dd787ec8efe8 100644 --- a/packages/service/core/workflow/dispatch/tools/runLoop.ts +++ b/packages/service/core/workflow/dispatch/tools/runLoop.ts @@ -13,7 +13,7 @@ type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.loopFlow]: { childNodes: Array }; }>; type Response = DispatchNodeResultType<{ - [NodeOutputKeyEnum.loopArray]: any[]; + [NodeOutputKeyEnum.loopArray]: Array; }>; export const dispatchLoop = async (props: Props): Promise => { 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 e50cbdeebb41..7fe2433cfb00 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 { useReactFlow, 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'; 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 f121e1e9a0bf..d97d739089d9 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 @@ -11,7 +11,9 @@ import { XYPosition, useReactFlow, getNodesBounds, - Rect + Rect, + NodeRemoveChange, + NodeSelectionChange } from 'reactflow'; import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import 'reactflow/dist/style.css'; @@ -21,7 +23,7 @@ import { useKeyboard } from './useKeyboard'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; -import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; /* @@ -274,6 +276,7 @@ export const useWorkflow = () => { onEdgesChange, setHoverEdgeId } = useContextSelector(WorkflowContext, (v) => v); + const { getIntersectingNodes } = useReactFlow(); const resetNodeSizeAndPosition = (rect: Rect, parentId: string) => { @@ -283,33 +286,35 @@ export const useWorkflow = () => { ); if (!loopFlow) return; + const newLoopFlowValue = { + ...loopFlow.value, + width: rect.width + 110 > 900 ? rect.width + 110 : 900, + height: rect.height + 380 > 900 ? rect.height + 380 : 900 + }; + onChangeNode({ nodeId: parentId || '', type: 'updateInput', key: NodeInputKeyEnum.loopFlow, value: { ...loopFlow, - value: { - ...loopFlow?.value, - width: rect.width + 110 > 900 ? rect.width + 110 : 900, - height: rect.height + 380 > 900 ? rect.height + 380 : 900 - } + value: newLoopFlowValue } }); - setNodes((nodes) => { - return nodes.map((node) => { - if (node.id === parentId) { - return { - ...node, - position: { - x: rect.x - 50, - y: rect.y - 280 + + setNodes((nodes) => + nodes.map((node) => + node.id === parentId + ? { + ...node, + position: { + x: rect.x - 50, + y: rect.y - 280 + } } - }; - } - return node; - }); - }); + : node + ) + ); }; /* helper line */ @@ -360,115 +365,136 @@ export const useWorkflow = () => { 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 if (node && nodes.find((item) => item.data.parentNodeId === node.id)) { - const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id); - const childNodesChange = childNodes.map((node) => { - return { - ...change, - id: node.id - }; - }); - return (() => { - onNodesChange(changes.concat(childNodesChange)); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) - ); - })(); - } else { - return (() => { - onNodesChange(changes); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) - ); - if (node?.data.parentNodeId) { - const parentId = node.data.parentNodeId; - const childNodes = nodes.filter( - (n) => n.data.parentNodeId === parentId && n.id !== node.id - ); - const parentNode = nodes.find((n) => n.id === parentId); - const rect = getNodesBounds(childNodes); - const updatedLoopFlow = parentNode?.data.inputs.find( - (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow - ); - if (updatedLoopFlow) { - updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.filter( - (nodeId: string) => nodeId !== node.id - ); - } - resetNodeSizeAndPosition(rect, parentId); - updatedLoopFlow && - onChangeNode({ - nodeId: parentId, - type: 'updateInput', - key: NodeInputKeyEnum.loopFlow, - value: updatedLoopFlow - }); - } - })(); + if (node) { + return handleRemoveNode(change, changes, node); } - } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { - change.selected = true; - } else if (change.type === 'position' && change.position) { + } else if (change.type === 'select') { + return handleSelectNode(change); + } else if (change.type === 'position') { const node = nodes.find((n) => n.id === change.id); - if (node && node.data.parentNodeId) { + if (node) { + return handlePositionNode(change, changes, node); + } + } + } + + customApplyNodeChanges(changes, nodes); + + onNodesChange(changes); + }; + + const handleRemoveNode = (change: NodeRemoveChange, changes: NodeChange[], node: Node) => { + if (node.data.forbidDelete) { + return toast({ + status: 'warning', + title: t('common:core.workflow.Can not delete node') + }); + } else if (nodes.some((n) => n.data.parentNodeId === node.id)) { + const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id); + const childNodesChange = childNodes.map((node) => ({ + ...change, + id: node.id + })); + return (() => { + onNodesChange([...changes, ...childNodesChange]); + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + })(); + } else { + return (() => { + onNodesChange(changes); + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + if (node?.data.parentNodeId) { const parentId = node.data.parentNodeId; + const childNodes = nodes.filter( + (n) => n.data.parentNodeId === parentId && n.id !== node.id + ); const parentNode = nodes.find((n) => n.id === parentId); - const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); - - if (!parentNode) return; const rect = getNodesBounds(childNodes); - return (() => { - customApplyNodeChanges(changes, childNodes); - onNodesChange(changes); + const updatedLoopFlow = parentNode?.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + if (updatedLoopFlow) { + updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.filter( + (nodeId: string) => nodeId !== node.id + ); resetNodeSizeAndPosition(rect, parentId); - })(); - } else if (node && nodes.find((item) => item.data.parentNodeId === node.id)) { - const parentId = node.id; - const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); - const initPosition = node.position; - const deltaX = change.position.x - initPosition.x; - const deltaY = change.position.y - initPosition.y; - const childNodesChange = childNodes.map((node) => { - if (change.dragging) { - return { - ...change, - id: node.id, - position: { - x: node.position.x + deltaX, - y: node.position.y + deltaY - }, - positionAbsolute: { - x: node.position.x + deltaX, - y: node.position.y + deltaY - } - }; - } else { - return { - ...change, - id: node.id - }; - } - }); - return (() => { - // customApplyNodeChanges( - // changes, - // nodes.filter((node) => !node.data.parentNodeId) - // ); - onNodesChange(changes.concat(childNodesChange)); - })(); + onChangeNode({ + nodeId: parentId, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: updatedLoopFlow + }); + } } - } + })(); } + }; - customApplyNodeChanges(changes, nodes); + const handleSelectNode = (change: NodeSelectionChange) => { + if (change.selected === false && isDowningCtrl) { + change.selected = true; + } + }; - onNodesChange(changes); + const handlePositionNode = (change: NodePositionChange, changes: NodeChange[], node: Node) => { + if (node.data.parentNodeId) { + const parentId = node.data.parentNodeId; + const parentNode = nodes.find((n) => n.id === parentId); + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); + + if (!parentNode) return; + const rect = getNodesBounds(childNodes); + return (() => { + customApplyNodeChanges(changes, childNodes); + onNodesChange(changes); + resetNodeSizeAndPosition(rect, parentId); + })(); + } else if (nodes.find((item) => item.data.parentNodeId === node.id)) { + 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 = childNodes.map((node) => { + if (change.dragging) { + return { + ...change, + id: node.id, + position: { + x: node.position.x + deltaX, + y: node.position.y + deltaY + }, + positionAbsolute: { + x: node.position.x + deltaX, + y: node.position.y + deltaY + } + }; + } else { + return { + ...change, + id: node.id + }; + } + }); + return (() => { + // customApplyNodeChanges( + // changes, + // nodes.filter((node) => !node.data.parentNodeId) + // ); + onNodesChange(changes.concat(childNodesChange)); + })(); + } else { + return (() => { + customApplyNodeChanges(changes, nodes); + onNodesChange(changes); + })(); + } }; + const handleEdgeChange = useCallback( (changes: EdgeChange[]) => { onEdgesChange(changes.filter((change) => change.type !== 'remove')); @@ -479,7 +505,7 @@ export const useWorkflow = () => { const onNodeDragStop = useCallback( (_: any, node: Node) => { const intersections = getIntersectingNodes(node); - const parentNode = intersections.find((item) => item.type === 'loop'); + const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop); const unSupportedTypes = [ FlowNodeTypeEnum.workflowStart, @@ -500,7 +526,7 @@ export const useWorkflow = () => { (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow ); if (updatedLoopFlow) { - updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.concat(node.id); + updatedLoopFlow.value.childNodes = [...updatedLoopFlow.value.childNodes, node.id]; } onChangeNode({ nodeId: node.id, @@ -518,7 +544,7 @@ export const useWorkflow = () => { state.filter((edge) => edge.source !== node.id && edge.target !== node.id) ); - const childNodes = nodes.filter((n) => n.data.parentNodeId === parentNode.id).concat(node); + const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node]; const rect = getNodesBounds(childNodes); resetNodeSizeAndPosition(rect, parentNode.id); } diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx index cb419a1536f0..a1b5a1204c7e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx @@ -96,7 +96,6 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { Date: Thu, 12 Sep 2024 11:10:25 +0800 Subject: [PATCH 4/7] fix version --- .../components/WorkflowComponents/AppCard.tsx | 6 ++---- .../Flow/hooks/useWorkflow.tsx | 9 +++++++-- .../components/WorkflowComponents/context.tsx | 17 ++++++++++++++--- .../components/WorkflowComponents/utils.tsx | 3 ++- projects/app/src/web/core/workflow/utils.ts | 2 +- 5 files changed, 26 insertions(+), 11 deletions(-) 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 000d4c674259..4629f5090f6e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx @@ -28,7 +28,6 @@ const AppCard = ({ isPublished: boolean; }) => { const { t } = useTranslation(); - const { appT } = useI18n(); const { feConfigs } = useSystemStore(); const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } = @@ -48,7 +47,7 @@ const AppCard = ({ children: [ { icon: 'edit', - label: appT('edit_info'), + label: t('app:edit_info'), onClick: onOpenInfoEdit }, { @@ -63,7 +62,7 @@ const AppCard = ({ { children: [ { - label: appT('import_configs'), + label: t('app:import_configs'), icon: 'common/importLight', onClick: onOpenImport }, @@ -117,7 +116,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/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index d97d739089d9..b100b42445f5 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 @@ -369,7 +369,7 @@ export const useWorkflow = () => { return handleRemoveNode(change, changes, node); } } else if (change.type === 'select') { - return handleSelectNode(change); + return handleSelectNode(change, changes); } else if (change.type === 'position') { const node = nodes.find((n) => n.id === change.id); if (node) { @@ -434,9 +434,14 @@ export const useWorkflow = () => { } }; - const handleSelectNode = (change: NodeSelectionChange) => { + const handleSelectNode = (change: NodeSelectionChange, changes: NodeChange[]) => { if (change.selected === false && isDowningCtrl) { change.selected = true; + } else { + return (() => { + customApplyNodeChanges(changes, nodes); + onNodesChange(changes); + })(); } }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index 707f8114793e..80ed35eccddd 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -569,7 +569,13 @@ const WorkflowContextProvider = ({ return resetSnapshot(past[0]); } - setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []); + setNodes( + e.nodes?.map((item) => + item.flowNodeType === FlowNodeTypeEnum.loop + ? storeNode2FlowNode({ item, t, zIndex: -1001 }) + : storeNode2FlowNode({ item, t }) + ) || [] + ); setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []); const chatConfig = e.chatConfig; @@ -583,7 +589,12 @@ const WorkflowContextProvider = ({ // If it is the initial data, save the initial snapshot if (isInit) { saveSnapshot({ - pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [], + pastNodes: + e.nodes?.map((item) => + item.flowNodeType === FlowNodeTypeEnum.loop + ? storeNode2FlowNode({ item, t, zIndex: -1001 }) + : storeNode2FlowNode({ item, t }) + ) || [], pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [], customTitle: t(`app:app.version_initial`), chatConfig: appDetail.chatConfig, @@ -612,7 +623,6 @@ const WorkflowContextProvider = ({ const flowData2StoreData = useMemoizedFn(() => { const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges }); - return storeNodes; }); @@ -878,6 +888,7 @@ const WorkflowContextProvider = ({ ); if (isPastEqual) return false; + console.log(currentNodes); setPast((past) => [ { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/utils.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/utils.tsx index 120eff2320f7..eb209819e835 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/utils.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/utils.tsx @@ -25,7 +25,8 @@ export const uiWorkflow2StoreWorkflow = ({ version: item.data.version, inputs: item.data.inputs, outputs: item.data.outputs, - pluginId: item.data.pluginId + pluginId: item.data.pluginId, + parentNodeId: item.data.parentNodeId })); // get all handle diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index b0c3db1d73ec..cc2b37d56a0b 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -97,11 +97,11 @@ export const storeNode2FlowNode = ({ // replace item data const nodeItem: FlowNodeItemType = { + parentNodeId, ...template, ...storeNode, avatar: template.avatar ?? storeNode.avatar, version: storeNode.version ?? template.version ?? defaultNodeVersion, - parentNodeId, /* Inputs and outputs, New fields are added, not reduced */ From ad3a9fc90af9310618b5a6c67a02b9342d481b36 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Thu, 12 Sep 2024 15:32:36 +0800 Subject: [PATCH 5/7] fix --- .../core/workflow/template/system/loopEnd.ts | 3 +- packages/global/core/workflow/type/io.d.ts | 1 + .../core/workflow/dispatch/tools/runLoop.ts | 10 ++++- .../chat/components/WholeResponseModal.tsx | 1 - .../Flow/hooks/useWorkflow.tsx | 29 +++++++------- .../Flow/nodes/NodeLoop.tsx | 6 +-- .../Flow/nodes/NodeLoopEnd.tsx | 12 +++--- .../Flow/nodes/NodeLoopStart.tsx | 4 +- .../RenderInput/templates/Reference.tsx | 40 ++++++++++++++++--- .../components/WorkflowComponents/context.tsx | 1 - 10 files changed, 72 insertions(+), 35 deletions(-) diff --git a/packages/global/core/workflow/template/system/loopEnd.ts b/packages/global/core/workflow/template/system/loopEnd.ts index cf32b9d19e84..c1abaebb092f 100644 --- a/packages/global/core/workflow/template/system/loopEnd.ts +++ b/packages/global/core/workflow/template/system/loopEnd.ts @@ -27,7 +27,8 @@ export const LoopEndNode: FlowNodeTemplateType = { valueType: WorkflowIOValueTypeEnum.any, label: '', required: true, - value: [] + value: [], + showType: true } ], outputs: [] diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index 988789ff59bc..11ef55fa909e 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -54,6 +54,7 @@ export type FlowNodeInputItemType = InputComponentPropsType & { // render components params canEdit?: boolean; // dynamic inputs isPro?: boolean; // Pro version field + showType?: boolean; // show data type }; export type FlowNodeOutputItemType = { diff --git a/packages/service/core/workflow/dispatch/tools/runLoop.ts b/packages/service/core/workflow/dispatch/tools/runLoop.ts index dd787ec8efe8..864585729288 100644 --- a/packages/service/core/workflow/dispatch/tools/runLoop.ts +++ b/packages/service/core/workflow/dispatch/tools/runLoop.ts @@ -25,8 +25,10 @@ export const dispatchLoop = async (props: Props): Promise => { const runNodes = runtimeNodes.filter((node) => childNodes.includes(node.nodeId)); const outputArray = []; const loopDetail: ChatHistoryItemResType[] = []; + let totalPoints = 0; + let totalTokens = 0; - for (const element of loopInputArray) { + for await (const element of loopInputArray) { const response = await dispatchWorkFlow({ ...props, runtimeNodes: runNodes.map((node) => @@ -54,10 +56,16 @@ export const dispatchLoop = async (props: Props): Promise => { )?.loopOutputElement; outputArray.push(loopOutputElement); loopDetail.push(...response.flowResponses); + response.flowResponses.forEach((res) => { + totalPoints = totalPoints + (res.totalPoints ?? 0); + totalTokens = totalTokens + (res.tokens ?? 0); + }); } return { [DispatchNodeResponseKeyEnum.nodeResponse]: { + totalPoints: totalPoints, + tokens: totalTokens, loopInput: loopInputArray, loopResult: outputArray, loopDetail: loopDetail diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index 848cb010fdd8..bc5dfd1ddd5a 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -39,7 +39,6 @@ export const WholeResponseContent = ({ showDetail: boolean; }) => { const { t } = useTranslation(); - console.log('activeModule', activeModule); // Auto scroll to top const ContentRef = useRef(null); 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 b100b42445f5..a7de76f64f9d 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 @@ -378,8 +378,6 @@ export const useWorkflow = () => { } } - customApplyNodeChanges(changes, nodes); - onNodesChange(changes); }; @@ -439,7 +437,6 @@ export const useWorkflow = () => { change.selected = true; } else { return (() => { - customApplyNodeChanges(changes, nodes); onNodesChange(changes); })(); } @@ -458,25 +455,23 @@ export const useWorkflow = () => { onNodesChange(changes); resetNodeSizeAndPosition(rect, parentId); })(); - } else if (nodes.find((item) => item.data.parentNodeId === node.id)) { + } else if (nodes.some((item) => item.data.parentNodeId === node.id)) { 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 = childNodes.map((node) => { + 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: { - x: node.position.x + deltaX, - y: node.position.y + deltaY - }, - positionAbsolute: { - x: node.position.x + deltaX, - y: node.position.y + deltaY - } + position, + positionAbsolute: position }; } else { return { @@ -490,11 +485,14 @@ export const useWorkflow = () => { // changes, // nodes.filter((node) => !node.data.parentNodeId) // ); - onNodesChange(changes.concat(childNodesChange)); + onNodesChange([...changes, ...childNodesChange]); })(); } else { return (() => { - customApplyNodeChanges(changes, nodes); + customApplyNodeChanges( + changes, + nodes.filter((node) => !node.data.parentNodeId) + ); onNodesChange(changes); })(); } @@ -509,6 +507,7 @@ export const useWorkflow = () => { const onNodeDragStop = useCallback( (_: any, node: Node) => { + if (!node) return; const intersections = getIntersectingNodes(node); const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx index faccb8cb1d15..7702463d675d 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx @@ -1,12 +1,12 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React, { useCallback, useEffect } from 'react'; -import { Background, NodeProps, NodeResizeControl, NodeResizer, OnResize } from 'reactflow'; +import React, { useEffect } 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, Center, Flex } from '@chakra-ui/react'; +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'; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx index cdbe6fbdb4de..c953c962fe06 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx @@ -1,16 +1,16 @@ 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 Reference from './render/RenderInput/templates/Reference'; import { Box } from '@chakra-ui/react'; +import React from 'react'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; const NodeLoopEnd = ({ data, selected }: NodeProps) => { - const { t } = useTranslation(); const { nodeId, inputs } = data; - const inputItem = inputs.find((input) => input.key === 'loopOutputArrayElement'); + const inputItem = inputs.find((input) => input.key === NodeInputKeyEnum.loopOutputArrayElement); + if (!inputItem) return null; return ( ) => { }} > - {inputItem && } + ); }; -export default NodeLoopEnd; +export default React.memo(NodeLoopEnd); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx index a1b5a1204c7e..e6183be11c1f 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx @@ -12,7 +12,7 @@ import { } from '@fastgpt/global/core/workflow/constants'; import VariableTable from './NodePluginIO/VariableTable'; import { Box } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; const typeMap = { @@ -109,4 +109,4 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { ); }; -export default NodeLoopStart; +export default React.memo(NodeLoopStart); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index ca11b22756e8..8818bb6df1cf 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -30,10 +30,12 @@ type SelectProps = { children: { label: string; value: string; + valueType?: WorkflowIOValueTypeEnum; }[]; }[]; onSelect: (val: ReferenceValueProps) => void; styles?: ButtonProps; + showType?: boolean; }; const Reference = ({ item, nodeId }: RenderInputProps) => { @@ -83,6 +85,7 @@ const Reference = ({ item, nodeId }: RenderInputProps) => { list={referenceList} value={formatValue} onSelect={onSelect} + showType={item.showType} /> ); }; @@ -139,7 +142,8 @@ export const useReference = ({ .map((output) => { return { label: t((output.label as any) || ''), - value: output.id + value: output.id, + valueType: output.valueType }; }) }; @@ -166,7 +170,13 @@ export const useReference = ({ formatValue }; }; -export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => { +export const ReferSelector = ({ + placeholder, + value, + list = [], + onSelect, + showType +}: SelectProps) => { const selectItemLabel = useMemo(() => { if (!value) { return; @@ -179,7 +189,12 @@ export const ReferSelector = ({ placeholder, value, list = [], onSelect }: Selec if (!secondColumn) { return; } - return [firstColumn, secondColumn]; + const valueType = secondColumn.valueType; + return { + firstColumn: firstColumn, + secondColumn: secondColumn, + valueType: valueType + }; }, [list, value]); const Render = useMemo(() => { @@ -188,9 +203,24 @@ export const ReferSelector = ({ placeholder, value, list = [], onSelect }: Selec label={ selectItemLabel ? ( - {selectItemLabel[0].label} + {selectItemLabel.firstColumn.label} - {selectItemLabel[1].label} + {selectItemLabel.secondColumn.label} + {showType && ( + + {selectItemLabel.valueType} + + )} ) : ( {placeholder} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index 80ed35eccddd..a5daf3facd80 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -888,7 +888,6 @@ const WorkflowContextProvider = ({ ); if (isPastEqual) return false; - console.log(currentNodes); setPast((past) => [ { From f904030203d6940749667a82de34accb51b7ded8 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Fri, 13 Sep 2024 15:36:22 +0800 Subject: [PATCH 6/7] fix --- .../global/core/workflow/runtime/type.d.ts | 9 +- .../core/workflow/template/constants.ts | 6 +- .../template/system/{ => loop}/loop.ts | 10 +- .../template/system/{ => loop}/loopEnd.ts | 10 +- .../template/system/{ => loop}/loopStart.ts | 10 +- .../core/workflow/dispatch/tools/runLoop.ts | 29 +++-- .../workflow/dispatch/tools/runLoopEnd.ts | 2 +- .../workflow/dispatch/tools/runLoopStart.ts | 2 +- .../web/components/common/EmptyTip/index.tsx | 7 +- .../plugins/VariablePickerPlugin/index.tsx | 2 +- .../chat/components/WholeResponseModal.tsx | 4 +- .../Flow/NodeTemplatesModal.tsx | 8 +- .../Flow/hooks/useKeyboard.tsx | 8 +- .../Flow/hooks/useWorkflow.tsx | 100 +++++++++-------- .../Flow/nodes/NodeLoop.tsx | 37 ++++--- .../Flow/nodes/NodeLoopEnd.tsx | 65 ++++++++++- .../Flow/nodes/NodeLoopStart.tsx | 102 +++++++++++------- .../Flow/nodes/NodePluginIO/VariableTable.tsx | 51 ++++----- .../Flow/nodes/render/NodeCard.tsx | 81 ++++---------- .../RenderInput/templates/Reference.tsx | 36 +------ 20 files changed, 308 insertions(+), 271 deletions(-) rename packages/global/core/workflow/template/system/{ => loop}/loop.ts (85%) rename packages/global/core/workflow/template/system/{ => loop}/loopEnd.ts (74%) rename packages/global/core/workflow/template/system/{ => loop}/loopStart.ts (74%) diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index a09bab79daf0..55f4776a0c54 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -173,15 +173,14 @@ export type DispatchNodeResponseType = { // update var updateVarResult?: any[]; - // loop start - loopInputElement?: any; - // loop end - loopOutputElement?: 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 0b583dfb2518..76439e3b3fd3 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -29,9 +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'; -import { LoopStartNode } from './system/loopStart'; -import { LoopEndNode } from './system/loopEnd'; +import { LoopNode } from './system/loop/loop'; +import { LoopStartNode } from './system/loop/loopStart'; +import { LoopEndNode } from './system/loop/loopEnd'; const systemNodes: FlowNodeTemplateType[] = [ AiChatModule, diff --git a/packages/global/core/workflow/template/system/loop.ts b/packages/global/core/workflow/template/system/loop/loop.ts similarity index 85% rename from packages/global/core/workflow/template/system/loop.ts rename to packages/global/core/workflow/template/system/loop/loop.ts index 1293335543f8..034bc74e118e 100644 --- a/packages/global/core/workflow/template/system/loop.ts +++ b/packages/global/core/workflow/template/system/loop/loop.ts @@ -2,16 +2,16 @@ import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum, FlowNodeTypeEnum -} from '../../node/constant'; -import { FlowNodeTemplateType } from '../../type/node.d'; +} 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'; +} from '../../../constants'; +import { getHandleConfig } from '../../utils'; +import { i18nT } from '../../../../../../web/i18n/utils'; export const LoopNode: FlowNodeTemplateType = { id: FlowNodeTypeEnum.loop, diff --git a/packages/global/core/workflow/template/system/loopEnd.ts b/packages/global/core/workflow/template/system/loop/loopEnd.ts similarity index 74% rename from packages/global/core/workflow/template/system/loopEnd.ts rename to packages/global/core/workflow/template/system/loop/loopEnd.ts index c1abaebb092f..48b05435207d 100644 --- a/packages/global/core/workflow/template/system/loopEnd.ts +++ b/packages/global/core/workflow/template/system/loop/loopEnd.ts @@ -1,12 +1,12 @@ -import { i18nT } from '../../../../../web/i18n/utils'; +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'; +} from '../../../constants'; +import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node'; +import { getHandleConfig } from '../../utils'; export const LoopEndNode: FlowNodeTemplateType = { id: FlowNodeTypeEnum.loopEnd, diff --git a/packages/global/core/workflow/template/system/loopStart.ts b/packages/global/core/workflow/template/system/loop/loopStart.ts similarity index 74% rename from packages/global/core/workflow/template/system/loopStart.ts rename to packages/global/core/workflow/template/system/loop/loopStart.ts index 405bf94eb542..a2a49e5d637b 100644 --- a/packages/global/core/workflow/template/system/loopStart.ts +++ b/packages/global/core/workflow/template/system/loop/loopStart.ts @@ -1,12 +1,12 @@ -import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../node/constant'; -import { FlowNodeTemplateType } from '../../type/node.d'; +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'; +} from '../../../constants'; +import { getHandleConfig } from '../../utils'; +import { i18nT } from '../../../../../../web/i18n/utils'; export const LoopStartNode: FlowNodeTemplateType = { id: FlowNodeTypeEnum.loopStart, diff --git a/packages/service/core/workflow/dispatch/tools/runLoop.ts b/packages/service/core/workflow/dispatch/tools/runLoop.ts index 864585729288..2d64c6b72f78 100644 --- a/packages/service/core/workflow/dispatch/tools/runLoop.ts +++ b/packages/service/core/workflow/dispatch/tools/runLoop.ts @@ -17,7 +17,12 @@ type Response = DispatchNodeResultType<{ }>; export const dispatchLoop = async (props: Props): Promise => { - const { params, runtimeNodes } = props; + const { + params, + runtimeNodes, + user, + node: { name } + } = props; const { loopInputArray, loopFlow: { childNodes } @@ -26,7 +31,6 @@ export const dispatchLoop = async (props: Props): Promise => { const outputArray = []; const loopDetail: ChatHistoryItemResType[] = []; let totalPoints = 0; - let totalTokens = 0; for await (const element of loopInputArray) { const response = await dispatchWorkFlow({ @@ -51,25 +55,30 @@ export const dispatchLoop = async (props: Props): Promise => { } ) }); - const loopOutputElement = response.flowResponses.find( + + const loopOutputValue = response.flowResponses.find( (res) => res.moduleType === FlowNodeTypeEnum.loopEnd - )?.loopOutputElement; - outputArray.push(loopOutputElement); + )?.loopOutputValue; + + outputArray.push(loopOutputValue); loopDetail.push(...response.flowResponses); - response.flowResponses.forEach((res) => { - totalPoints = totalPoints + (res.totalPoints ?? 0); - totalTokens = totalTokens + (res.tokens ?? 0); - }); + + totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); } return { [DispatchNodeResponseKeyEnum.nodeResponse]: { totalPoints: totalPoints, - tokens: totalTokens, loopInput: loopInputArray, loopResult: outputArray, loopDetail: loopDetail }, + [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ + { + totalPoints: user.openaiAccount?.key ? 0 : totalPoints, + moduleName: name + } + ], [NodeOutputKeyEnum.loopArray]: outputArray }; }; diff --git a/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts b/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts index ddc2638a8eaa..4c4725dc1623 100644 --- a/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts +++ b/packages/service/core/workflow/dispatch/tools/runLoopEnd.ts @@ -15,7 +15,7 @@ export const dispatchLoopEnd = async (props: Props): Promise => { return { [DispatchNodeResponseKeyEnum.nodeResponse]: { - loopOutputElement: params.loopOutputArrayElement + loopOutputValue: params.loopOutputArrayElement } }; }; diff --git a/packages/service/core/workflow/dispatch/tools/runLoopStart.ts b/packages/service/core/workflow/dispatch/tools/runLoopStart.ts index 938d38e4f183..ee36c1db8579 100644 --- a/packages/service/core/workflow/dispatch/tools/runLoopStart.ts +++ b/packages/service/core/workflow/dispatch/tools/runLoopStart.ts @@ -17,7 +17,7 @@ export const dispatchLoopStart = async (props: Props): Promise => { return { [DispatchNodeResponseKeyEnum.nodeResponse]: { - loopInputElement: params.loopArrayElement + loopInputValue: params.loopArrayElement }, [NodeOutputKeyEnum.loopArrayElement]: params.loopArrayElement }; diff --git a/packages/web/components/common/EmptyTip/index.tsx b/packages/web/components/common/EmptyTip/index.tsx index 5c8161a6f9cd..816112246449 100644 --- a/packages/web/components/common/EmptyTip/index.tsx +++ b/packages/web/components/common/EmptyTip/index.tsx @@ -5,15 +5,14 @@ import { useTranslation } from 'next-i18next'; type Props = FlexProps & { text?: string | React.ReactNode; - iconW?: string | number; - iconH?: string | number; + iconSize?: string | number; }; -const EmptyTip = ({ text, iconW, iconH, ...props }: Props) => { +const EmptyTip = ({ text, iconSize, ...props }: Props) => { const { t } = useTranslation(); return ( - + {text || t('common:common.empty.Common Tip')} 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 03158b4fd63a..45632b03edc6 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/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index bc5dfd1ddd5a..2c1d04ae2bfe 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -344,13 +344,13 @@ export const WholeResponseContent = ({ {/* loopStart */} {/* loopEnd */} ) : null; 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 7fe2433cfb00..8479acde4bc3 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 @@ -490,14 +490,10 @@ const RenderList = React.memo(function RenderList({ if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) { const loopStartNode = moduleTemplatesFlat.find( (item) => item.flowNodeType === FlowNodeTypeEnum.loopStart - ); + )!; const loopEndNode = moduleTemplatesFlat.find( (item) => item.flowNodeType === FlowNodeTypeEnum.loopEnd - ); - - if (!loopStartNode || !loopEndNode) { - return; - } + )!; const startNode = nodeTemplate2FlowNode({ template: loopStartNode, 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 cfe61d1c28d9..63448c112362 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, 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 a7de76f64f9d..579c42bc7375 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 @@ -360,6 +360,58 @@ export const useWorkflow = () => { } }; + // Check if a node is placed on top of a loop node + const checkNodeOverLoopNode = useCallback( + (node: Node) => { + if (!node) return; + const intersections = getIntersectingNodes(node); + 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') + }); + } + const updatedLoopFlow = parentNode.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + if (updatedLoopFlow) { + updatedLoopFlow.value.childNodes = [...updatedLoopFlow.value.childNodes, node.id]; + } + onChangeNode({ + nodeId: node.id, + type: 'attr', + key: 'parentNodeId', + value: parentNode.id + }); + onChangeNode({ + nodeId: parentNode.id, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: updatedLoopFlow + }); + 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); + resetNodeSizeAndPosition(rect, parentNode.id); + } + }, + [getIntersectingNodes, onChangeNode, setEdges, nodes, toast, t, resetNodeSizeAndPosition] + ); + /* node */ const handleNodesChange = (changes: NodeChange[]) => { for (const change of changes) { @@ -507,53 +559,9 @@ export const useWorkflow = () => { const onNodeDragStop = useCallback( (_: any, node: Node) => { - if (!node) return; - const intersections = getIntersectingNodes(node); - 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') - }); - } - const updatedLoopFlow = parentNode.data.inputs.find( - (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow - ); - if (updatedLoopFlow) { - updatedLoopFlow.value.childNodes = [...updatedLoopFlow.value.childNodes, node.id]; - } - onChangeNode({ - nodeId: node.id, - type: 'attr', - key: 'parentNodeId', - value: parentNode.id - }); - onChangeNode({ - nodeId: parentNode.id, - type: 'updateInput', - key: NodeInputKeyEnum.loopFlow, - value: updatedLoopFlow - }); - 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); - resetNodeSizeAndPosition(rect, parentNode.id); - } + checkNodeOverLoopNode(node); }, - [getIntersectingNodes, onChangeNode, setEdges, nodes, toast, t, resetNodeSizeAndPosition] + [checkNodeOverLoopNode] ); /* connect */ diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx index 7702463d675d..900f612d9f81 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx @@ -1,5 +1,5 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Background, NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import Container from '../components/Container'; @@ -18,23 +18,29 @@ const NodeLoop = ({ data, selected }: NodeProps) => { const { nodeId, inputs, outputs } = data; const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); - const loopFlowData = inputs.find((input) => input.key === NodeInputKeyEnum.loopFlow); - const childNodes = nodeList.filter((node) => node.parentNodeId === nodeId); + const loopFlowData = useMemo( + () => inputs.find((input) => input.key === NodeInputKeyEnum.loopFlow), + [inputs] + ); + const childNodes = useMemo( + () => nodeList.filter((node) => node.parentNodeId === nodeId), + [nodeList, nodeId] + ); useEffect(() => { - loopFlowData && - onChangeNode({ - nodeId, - type: 'updateInput', - key: NodeInputKeyEnum.loopFlow, + if (!loopFlowData) return; + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: { + ...loopFlowData, value: { - ...loopFlowData, - value: { - ...loopFlowData?.value, - childNodes: childNodes.map((node) => node.nodeId) - } + ...loopFlowData?.value, + childNodes: childNodes.map((node) => node.nodeId) } - }); + } + }); }, []); return ( @@ -45,6 +51,9 @@ const NodeLoop = ({ data, selected }: NodeProps) => { minH={900} w={loopFlowData?.value?.width} h={loopFlowData?.value?.height} + menuForbid={{ + copy: true + }} {...data} > diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx index c953c962fe06..2e75139f008a 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx @@ -3,14 +3,73 @@ import { NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import Reference from './render/RenderInput/templates/Reference'; import { Box } from '@chakra-ui/react'; -import React from 'react'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import React, { useEffect, useMemo, useCallback } 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 } = data; - const inputItem = inputs.find((input) => input.key === NodeInputKeyEnum.loopOutputArrayElement); + 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.loopOutputArrayElement), + [inputs] + ); if (!inputItem) return null; + + const global = getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig }); + const inputItemValueNode = useMemo( + () => [...nodeList, global].find((node) => node.nodeId === inputItem.value[0]), + [nodeList, inputItem] + ); + const inputItemValueOutput = useMemo( + () => inputItemValueNode?.outputs.find((output) => output.id === inputItem.value[1]), + [inputItemValueNode] + ); + const valueType = inputItemValueOutput?.valueType; + + useEffect(() => { + if (valueType) { + const currentNode = nodeList.find((node) => node.nodeId === nodeId); + const parentNode = nodeList.find((node) => node.nodeId === currentNode?.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 as keyof typeof typeMap] ?? WorkflowIOValueTypeEnum.arrayAny + } + }); + } + } + }, [valueType, nodeList, nodeId, onChangeNode, typeMap]); + return ( ) => { const { t } = useTranslation(); const { nodeId } = data; - const { nodes, nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); - const loopStartNode = nodes.find((node) => node.id === nodeId); - const parentNode = nodes.find((node) => node.id === loopStartNode?.data.parentNodeId); - const arrayInput = parentNode?.data.inputs.find( + const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); + + const loopStartNode = nodeList.find((node) => node.nodeId === nodeId); + const parentNode = nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId); + const arrayInput = parentNode?.inputs.find( (input) => input.key === NodeInputKeyEnum.loopInputArray ); - const outputValueType = !!arrayInput?.value ? nodeList .find((node) => node.nodeId === arrayInput?.value[0]) ?.outputs.find((output) => output.id === arrayInput?.value[1])?.valueType : undefined; + const variables = [ + { + icon: 'core/workflow/inputType/array', + label: '数组元素', + type: typeMap[outputValueType as keyof typeof typeMap], + key: t('workflow:Array_element') + } + ]; useEffect(() => { - if ( - !outputValueType && - loopStartNode?.data.outputs.find( - (output) => output.key === NodeOutputKeyEnum.loopArrayElement - ) - ) { + const loopArrayOutput = loopStartNode?.outputs.find( + (output) => output.key === NodeOutputKeyEnum.loopArrayElement + ); + + if (!outputValueType && loopArrayOutput) { onChangeNode({ nodeId, type: 'delOutput', key: NodeOutputKeyEnum.loopArrayElement }); - } else if ( - outputValueType && - !loopStartNode?.data.outputs.find( - (output) => output.key === NodeOutputKeyEnum.loopArrayElement - ) - ) { + } else if (outputValueType && !loopArrayOutput) { onChangeNode({ nodeId, type: 'addOutput', @@ -68,6 +70,16 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { valueType: typeMap[outputValueType as keyof typeof typeMap] } }); + } else if (outputValueType && loopArrayOutput) { + onChangeNode({ + nodeId, + type: 'updateOutput', + key: NodeOutputKeyEnum.loopArrayElement, + value: { + ...loopArrayOutput, + valueType: typeMap[outputValueType as keyof typeof typeMap] + } + }); } }, [onChangeNode, outputValueType]); @@ -85,24 +97,42 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { > {!outputValueType ? ( - + ) : ( - + + + + + + + + + + + {variables.map((item) => ( + + + + + ))} + +
+ {t('common:core.module.variable.variable name')} + {t('common:core.workflow.Value type')}
+ + {!!item.icon && ( + + )} + {item.label || item.key} + + {item.type}
+
+
)}
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 2e6657d1d340..0bf15c29204c 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 = [], @@ -10,8 +9,8 @@ const VariableTable = ({ onDelete }: { variables: { icon?: string; label: string; type: string; key: string; isTool?: boolean }[]; - onEdit?: (key: string) => void; - onDelete?: (key: string) => void; + onEdit: (key: string) => void; + onDelete: (key: string) => void; }) => { const { t } = useTranslation(); const showToolColumn = variables.some((item) => item.isTool); @@ -38,38 +37,32 @@ const VariableTable = ({ {!!item.icon && ( )} - {/* */} {item.label || item.key} - {/* */} {item.type} {showToolColumn && {item.isTool ? '✅' : '-'}} - {onEdit && ( - onEdit(item.key)} - /> - )} - {onDelete && ( - { - onDelete(item.key); - }} - /> - )} + onEdit(item.key)} + /> + { + onDelete(item.key); + }} + /> ))} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index fb070bfab193..6c87a94a1686 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -341,67 +341,26 @@ const MenuRender = React.memo(function MenuRender({ pluginId: node.data.pluginId, version: node.data.version }; - const childNodes = state.filter((item) => item.data.parentNodeId === nodeId); - - const childNodeTemplates = childNodes.map((item) => ({ - avatar: item.data.avatar, - name: computedNewNodeName({ - templateName: item.data.name, - flowNodeType: item.data.flowNodeType, - pluginId: item.data.pluginId - }), - intro: item.data.intro, - flowNodeType: item.data.flowNodeType, - inputs: item.data.inputs, - outputs: item.data.outputs, - showStatus: item.data.showStatus, - pluginId: item.data.pluginId, - version: item.data.version, - position: { x: item.position.x + 200, y: item.position.y + 50 } - })); - const currentNodeId = getNanoid(); - return state - .concat( - storeNode2FlowNode({ - item: { - flowNodeType: template.flowNodeType, - avatar: template.avatar, - name: template.name, - intro: template.intro, - nodeId: currentNodeId, - position: { x: node.position.x + 200, y: node.position.y + 50 }, - showStatus: template.showStatus, - pluginId: template.pluginId, - inputs: template.inputs, - outputs: template.outputs, - version: template.version - }, - selected: true, - zIndex: childNodes.length > 0 ? -1001 : 0, - t - }) - ) - .concat( - childNodeTemplates.map((template) => - storeNode2FlowNode({ - item: { - flowNodeType: template.flowNodeType, - avatar: template.avatar, - name: template.name, - intro: template.intro, - nodeId: getNanoid(), - position: template.position, - showStatus: template.showStatus, - pluginId: template.pluginId, - inputs: template.inputs, - outputs: template.outputs, - version: template.version - }, - parentNodeId: currentNodeId, - t - }) - ) - ); + return state.concat( + storeNode2FlowNode({ + item: { + flowNodeType: template.flowNodeType, + avatar: template.avatar, + name: template.name, + intro: template.intro, + nodeId: getNanoid(), + position: { x: node.position.x + 200, y: node.position.y + 50 }, + showStatus: template.showStatus, + pluginId: template.pluginId, + inputs: template.inputs, + outputs: template.outputs, + version: template.version + }, + selected: true, + parentNodeId: undefined, + t + }) + ); }); }, [computedNewNodeName, setNodes, t] diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 8818bb6df1cf..397c0d7d248e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -35,7 +35,6 @@ type SelectProps = { }[]; onSelect: (val: ReferenceValueProps) => void; styles?: ButtonProps; - showType?: boolean; }; const Reference = ({ item, nodeId }: RenderInputProps) => { @@ -85,7 +84,6 @@ const Reference = ({ item, nodeId }: RenderInputProps) => { list={referenceList} value={formatValue} onSelect={onSelect} - showType={item.showType} /> ); }; @@ -170,13 +168,7 @@ export const useReference = ({ formatValue }; }; -export const ReferSelector = ({ - placeholder, - value, - list = [], - onSelect, - showType -}: SelectProps) => { +export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => { const selectItemLabel = useMemo(() => { if (!value) { return; @@ -189,12 +181,7 @@ export const ReferSelector = ({ if (!secondColumn) { return; } - const valueType = secondColumn.valueType; - return { - firstColumn: firstColumn, - secondColumn: secondColumn, - valueType: valueType - }; + return [firstColumn, secondColumn]; }, [list, value]); const Render = useMemo(() => { @@ -203,24 +190,9 @@ export const ReferSelector = ({ label={ selectItemLabel ? ( - {selectItemLabel.firstColumn.label} + {selectItemLabel[0].label} - {selectItemLabel.secondColumn.label} - {showType && ( - - {selectItemLabel.valueType} - - )} + {selectItemLabel[1].label} ) : ( {placeholder} From e7d7cd1863afb26264ac7b3521be19a590a1de6e Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Fri, 13 Sep 2024 18:35:25 +0800 Subject: [PATCH 7/7] fix --- .../Flow/NodeTemplatesModal.tsx | 14 +- .../Flow/hooks/useWorkflow.tsx | 163 +++++++++--------- .../Flow/nodes/NodeLoop.tsx | 6 + .../Flow/nodes/NodeLoopEnd.tsx | 15 +- .../Flow/nodes/NodeLoopStart.tsx | 59 ++++--- .../Flow/nodes/render/NodeCard.tsx | 8 +- .../components/WorkflowComponents/context.tsx | 21 +-- 7 files changed, 151 insertions(+), 135 deletions(-) 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 8479acde4bc3..c0242032e92d 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 @@ -47,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; @@ -482,27 +484,19 @@ const RenderList = React.memo(function RenderList({ }, position: { x: mouseX, y: mouseY }, selected: true, - zIndex: templateNode.flowNodeType === FlowNodeTypeEnum.loop ? -1001 : 0, t }); const newNodes = [newNode]; if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) { - const loopStartNode = moduleTemplatesFlat.find( - (item) => item.flowNodeType === FlowNodeTypeEnum.loopStart - )!; - const loopEndNode = moduleTemplatesFlat.find( - (item) => item.flowNodeType === FlowNodeTypeEnum.loopEnd - )!; - const startNode = nodeTemplate2FlowNode({ - template: loopStartNode, + template: LoopStartNode, position: { x: mouseX + 60, y: mouseY + 280 }, parentNodeId: newNode.id, t }); const endNode = nodeTemplate2FlowNode({ - template: loopEndNode, + template: LoopEndNode, position: { x: mouseX + 420, y: mouseY + 680 }, parentNodeId: newNode.id, t 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 579c42bc7375..caf450f29d67 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 @@ -382,12 +382,14 @@ export const useWorkflow = () => { title: t('workflow:can_not_loop') }); } + const updatedLoopFlow = parentNode.data.inputs.find( (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow ); if (updatedLoopFlow) { updatedLoopFlow.value.childNodes = [...updatedLoopFlow.value.childNodes, node.id]; } + onChangeNode({ nodeId: node.id, type: 'attr', @@ -413,88 +415,68 @@ export const useWorkflow = () => { ); /* node */ - const handleNodesChange = (changes: NodeChange[]) => { - for (const change of changes) { - if (change.type === 'remove') { - const node = nodes.find((n) => n.id === change.id); - if (node) { - return handleRemoveNode(change, changes, node); - } - } else if (change.type === 'select') { - return handleSelectNode(change, changes); - } else if (change.type === 'position') { - const node = nodes.find((n) => n.id === change.id); - if (node) { - return handlePositionNode(change, changes, node); - } - } - } - - onNodesChange(changes); - }; - const handleRemoveNode = (change: NodeRemoveChange, changes: NodeChange[], node: Node) => { if (node.data.forbidDelete) { return toast({ status: 'warning', title: t('common:core.workflow.Can not delete node') }); - } else if (nodes.some((n) => n.data.parentNodeId === node.id)) { + } + + // 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 childNodesChange = childNodes.map((node) => ({ ...change, id: node.id })); - return (() => { - onNodesChange([...changes, ...childNodesChange]); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) - ); - })(); - } else { - return (() => { - onNodesChange(changes); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + onNodesChange([...changes, ...childNodesChange]); + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + return; + } + + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + onNodesChange(changes); + + // If the node is a child node, remove the child node from the parent node + if (node?.data.parentNodeId) { + const parentId = node.data.parentNodeId; + const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId && n.id !== node.id); + const parentNode = nodes.find((n) => n.id === parentId); + const rect = getNodesBounds(childNodes); + const updatedLoopFlow = parentNode?.data.inputs.find( + (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow + ); + if (updatedLoopFlow) { + updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.filter( + (nodeId: string) => nodeId !== node.id ); - if (node?.data.parentNodeId) { - const parentId = node.data.parentNodeId; - const childNodes = nodes.filter( - (n) => n.data.parentNodeId === parentId && n.id !== node.id - ); - const parentNode = nodes.find((n) => n.id === parentId); - const rect = getNodesBounds(childNodes); - const updatedLoopFlow = parentNode?.data.inputs.find( - (input: FlowNodeInputItemType) => input.key === NodeInputKeyEnum.loopFlow - ); - if (updatedLoopFlow) { - updatedLoopFlow.value.childNodes = updatedLoopFlow.value.childNodes.filter( - (nodeId: string) => nodeId !== node.id - ); - resetNodeSizeAndPosition(rect, parentId); - onChangeNode({ - nodeId: parentId, - type: 'updateInput', - key: NodeInputKeyEnum.loopFlow, - value: updatedLoopFlow - }); - } - } - })(); + resetNodeSizeAndPosition(rect, parentId); + onChangeNode({ + nodeId: parentId, + type: 'updateInput', + key: NodeInputKeyEnum.loopFlow, + value: updatedLoopFlow + }); + } } }; const handleSelectNode = (change: NodeSelectionChange, changes: NodeChange[]) => { + // If the node is not selected and the Ctrl key is pressed, select the node if (change.selected === false && isDowningCtrl) { change.selected = true; - } else { - return (() => { - onNodesChange(changes); - })(); } + + onNodesChange(changes); }; const handlePositionNode = (change: NodePositionChange, changes: NodeChange[], node: Node) => { + // If node is a child node, move child node and reset parent node if (node.data.parentNodeId) { const parentId = node.data.parentNodeId; const parentNode = nodes.find((n) => n.id === parentId); @@ -502,12 +484,13 @@ export const useWorkflow = () => { if (!parentNode) return; const rect = getNodesBounds(childNodes); - return (() => { - customApplyNodeChanges(changes, childNodes); - onNodesChange(changes); - resetNodeSizeAndPosition(rect, parentId); - })(); - } else if (nodes.some((item) => item.data.parentNodeId === node.id)) { + customApplyNodeChanges(changes, childNodes); + onNodesChange(changes); + resetNodeSizeAndPosition(rect, parentId); + return; + } + // If node is parent node, move parent node and child nodes + if (nodes.some((item) => item.data.parentNodeId === node.id)) { const parentId = node.id; const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); const initPosition = node.position; @@ -532,22 +515,40 @@ export const useWorkflow = () => { }; } }); - return (() => { - // customApplyNodeChanges( - // changes, - // nodes.filter((node) => !node.data.parentNodeId) - // ); - onNodesChange([...changes, ...childNodesChange]); - })(); - } else { - return (() => { - customApplyNodeChanges( - changes, - nodes.filter((node) => !node.data.parentNodeId) - ); - onNodesChange(changes); - })(); + // customApplyNodeChanges( + // changes, + // nodes.filter((node) => !node.data.parentNodeId) + // ); + onNodesChange([...changes, ...childNodesChange]); + return; } + + customApplyNodeChanges( + changes, + nodes.filter((node) => !node.data.parentNodeId) + ); + onNodesChange(changes); + }; + + const handleNodesChange = (changes: NodeChange[]) => { + for (const change of changes) { + if (change.type === 'remove') { + const node = nodes.find((n) => n.id === change.id); + if (node) { + return handleRemoveNode(change, changes, node); + } + } else if (change.type === 'select') { + return handleSelectNode(change, changes); + } else if (change.type === 'position') { + const node = nodes.find((n) => n.id === change.id); + if (node) { + return handlePositionNode(change, changes, node); + } + } + } + + // default changes + onNodesChange(changes); }; const handleEdgeChange = useCallback( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx index 900f612d9f81..b1c4b67270c7 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoop.tsx @@ -1,3 +1,9 @@ +/* + 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'; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx index 2e75139f008a..71caad0693a2 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopEnd.tsx @@ -3,7 +3,7 @@ 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, useCallback } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { NodeInputKeyEnum, NodeOutputKeyEnum, @@ -34,15 +34,16 @@ const NodeLoopEnd = ({ data, selected }: NodeProps) => { [inputs] ); - if (!inputItem) return null; - - const global = getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig }); + const global = useMemo( + () => getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig }), + [nodeList, t, appDetail.chatConfig] + ); const inputItemValueNode = useMemo( - () => [...nodeList, global].find((node) => node.nodeId === inputItem.value[0]), + () => [...nodeList, global].find((node) => node.nodeId === inputItem?.value[0]), [nodeList, inputItem] ); const inputItemValueOutput = useMemo( - () => inputItemValueNode?.outputs.find((output) => output.id === inputItem.value[1]), + () => inputItemValueNode?.outputs.find((output) => output.id === inputItem?.value[1]), [inputItemValueNode] ); const valueType = inputItemValueOutput?.valueType; @@ -82,7 +83,7 @@ const NodeLoopEnd = ({ data, selected }: NodeProps) => { }} > - + {inputItem && } ); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx index 7af28d17638c..bf7ea0c41b0e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeLoopStart.tsx @@ -11,7 +11,7 @@ import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -28,37 +28,54 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { const { nodeId } = data; const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); - const loopStartNode = nodeList.find((node) => node.nodeId === nodeId); - const parentNode = nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId); - const arrayInput = parentNode?.inputs.find( - (input) => input.key === NodeInputKeyEnum.loopInputArray + const loopStartNode = useMemo( + () => nodeList.find((node) => node.nodeId === nodeId), + [nodeList, nodeId] + ); + const parentNode = useMemo( + () => nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId), + [nodeList, loopStartNode] + ); + const arrayInput = useMemo( + () => parentNode?.inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray), + [parentNode] + ); + const outputValueType = useMemo( + () => + !!arrayInput?.value + ? nodeList + .find((node) => node.nodeId === arrayInput?.value[0]) + ?.outputs.find((output) => output.id === arrayInput?.value[1])?.valueType + : undefined, + [arrayInput, nodeList] + ); + const variables = useMemo( + () => [ + { + icon: 'core/workflow/inputType/array', + label: '数组元素', + type: typeMap[outputValueType as keyof typeof typeMap], + key: t('workflow:Array_element') + } + ], + [outputValueType, t] ); - const outputValueType = !!arrayInput?.value - ? nodeList - .find((node) => node.nodeId === arrayInput?.value[0]) - ?.outputs.find((output) => output.id === arrayInput?.value[1])?.valueType - : undefined; - const variables = [ - { - icon: 'core/workflow/inputType/array', - label: '数组元素', - type: typeMap[outputValueType as keyof typeof typeMap], - key: t('workflow:Array_element') - } - ]; useEffect(() => { const loopArrayOutput = loopStartNode?.outputs.find( (output) => output.key === NodeOutputKeyEnum.loopArrayElement ); + // if outputValueType is undefined, delete loopArrayElement output if (!outputValueType && loopArrayOutput) { onChangeNode({ nodeId, type: 'delOutput', key: NodeOutputKeyEnum.loopArrayElement }); - } else if (outputValueType && !loopArrayOutput) { + } + // if outputValueType is not undefined, and has no loopArrayOutput, add loopArrayElement output + if (outputValueType && !loopArrayOutput) { onChangeNode({ nodeId, type: 'addOutput', @@ -70,7 +87,9 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { valueType: typeMap[outputValueType as keyof typeof typeMap] } }); - } else if (outputValueType && loopArrayOutput) { + } + // if outputValueType is not undefined, and has loopArrayOutput, update loopArrayElement output + if (outputValueType && loopArrayOutput) { onChangeNode({ nodeId, type: 'updateOutput', diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 6c87a94a1686..900ab9fb5489 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -54,8 +54,8 @@ const NodeCard = (props: Props) => { minW = '300px', maxW = '600px', minH = 0, - w, - h, + w = 'full', + h = 'full', nodeId, selected, menuForbid, @@ -270,8 +270,8 @@ const NodeCard = (props: Props) => { borderWidth={'1px'} borderRadius={'md'} boxShadow={'1'} - w={w || 'full'} - h={h || 'full'} + w={w} + h={h} _hover={{ boxShadow: '4', '& .controller-menu': { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index a5daf3facd80..321a77e54852 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -403,6 +403,12 @@ const WorkflowContextProvider = ({ const [nodes = [], setNodes, onNodesChange] = useNodesState([]); const [hoverNodeId, setHoverNodeId] = useState(); + useEffect(() => { + setNodes((nodes) => + nodes.map((node) => (node.type === FlowNodeTypeEnum.loop ? { ...node, zIndex: -1001 } : node)) + ); + }, [nodes.length]); + const nodeListString = JSON.stringify(nodes.map((node) => node.data)); const nodeList = useMemo( () => JSON.parse(nodeListString) as FlowNodeItemType[], @@ -569,13 +575,7 @@ const WorkflowContextProvider = ({ return resetSnapshot(past[0]); } - setNodes( - e.nodes?.map((item) => - item.flowNodeType === FlowNodeTypeEnum.loop - ? storeNode2FlowNode({ item, t, zIndex: -1001 }) - : storeNode2FlowNode({ item, t }) - ) || [] - ); + setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []); setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []); const chatConfig = e.chatConfig; @@ -589,12 +589,7 @@ const WorkflowContextProvider = ({ // If it is the initial data, save the initial snapshot if (isInit) { saveSnapshot({ - pastNodes: - e.nodes?.map((item) => - item.flowNodeType === FlowNodeTypeEnum.loop - ? storeNode2FlowNode({ item, t, zIndex: -1001 }) - : storeNode2FlowNode({ item, t }) - ) || [], + pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [], pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [], customTitle: t(`app:app.version_initial`), chatConfig: appDetail.chatConfig,