From 8ede7add011783fbccb399db2c3dc7105a4c82bc Mon Sep 17 00:00:00 2001 From: heheer Date: Fri, 8 Nov 2024 12:10:15 +0800 Subject: [PATCH] loop node dynamic height (#3092) * loop node dynamic height * fix * fix --- .../global/core/workflow/runtime/utils.ts | 12 ++- .../global/core/workflow/template/input.ts | 2 +- packages/global/core/workflow/utils.ts | 9 ++ .../service/core/workflow/dispatch/index.ts | 2 +- .../Flow/components/FlowController.tsx | 23 +++-- .../Flow/hooks/useWorkflow.tsx | 99 +++++++++++-------- .../Flow/nodes/Loop/NodeLoop.tsx | 38 ++++--- .../templates/DynamicInputs/index.tsx | 15 ++- .../RenderInput/templates/Reference.tsx | 61 +++++------- .../WorkflowComponents/context/index.tsx | 1 - projects/app/src/web/core/workflow/utils.ts | 32 ++++-- 11 files changed, 176 insertions(+), 118 deletions(-) diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 6ce5be234..2e27bed0e 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -5,7 +5,7 @@ import { StoreNodeItemType } from '../type/node'; import { StoreEdgeItemType } from '../type/edge'; import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type'; import { VARIABLE_NODE_ID } from '../constants'; -import { isReferenceValue, isReferenceValueArray } from '../utils'; +import { isReferenceValueFormat } from '../utils'; import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; @@ -244,7 +244,7 @@ export const getReferenceVariableValue = ({ const nodeIds = nodes.map((node) => node.nodeId); // handle single reference value - if (isReferenceValue(value, nodeIds)) { + if (isReferenceValueFormat(value)) { const sourceNodeId = value[0]; const outputId = value[1]; @@ -261,7 +261,11 @@ export const getReferenceVariableValue = ({ } // handle reference array - if (isReferenceValueArray(value, nodeIds)) { + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item) => isReferenceValueFormat(item)) + ) { const result = value.map((val) => { return getReferenceVariableValue({ value: val, @@ -270,7 +274,7 @@ export const getReferenceVariableValue = ({ }); }); - return result.flat(); + return result.flat().filter((item) => item !== undefined); } return value; diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index 6a73b5e6e..a585d9651 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -111,7 +111,7 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = { renderTypeList: [FlowNodeInputTypeEnum.hidden], valueType: WorkflowIOValueTypeEnum.number, label: '', - value: 960 + value: 600 }; export const Input_Template_Stream_MODE: FlowNodeInputItemType = { diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 4cbdd5f70..5e9337859 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -306,6 +306,15 @@ export const isReferenceValue = (value: any, nodeIds: string[]): value is [strin const validIdSet = new Set([VARIABLE_NODE_ID, ...nodeIds]); return validIdSet.has(value[0]); }; + +export const isReferenceValueFormat = (value: any): value is [string, string] => { + return ( + Array.isArray(value) && + value.length === 2 && + typeof value[0] === 'string' && + typeof value[1] === 'string' + ); +}; export const isReferenceValueArray = ( value: any, nodeIds: string[] diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index cba7e68fe..88ac17115 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -501,7 +501,7 @@ export async function dispatchWorkFlow(data: Props): Promise { + useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => { + e.preventDefault(); + e.stopPropagation(); if (!mouseInCanvas) return; - undo(); - }); - useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => { - if (!mouseInCanvas) return; - redo(); + + const isUndo = e.key.toLowerCase() === 'z' && !e.shiftKey; + const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y'; + + if (isUndo) { + undo(); + } else if (isRedo) { + redo(); + } }); + useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => { e.preventDefault(); + e.stopPropagation(); if (!mouseInCanvas) return; zoomIn(); }); 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 f2383dda9..ef28baa43 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 @@ -293,9 +293,24 @@ export const useWorkflow = () => { const { isDowningCtrl } = useKeyboard(); // Loop node size and position - const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => { - const width = rect.width + 110 > 900 ? rect.width + 110 : 900; - const height = rect.height + 420 > 900 ? rect.height + 420 : 900; + const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => { + const { childNodes, loopNode } = nodes.reduce( + (acc, node) => { + if (node.data.parentNodeId === parentId) { + acc.childNodes.push(node); + } + if (node.id === parentId) { + acc.loopNode = node; + } + return acc; + }, + { childNodes: [] as Node[], loopNode: undefined as Node | undefined } + ); + const rect = getNodesBounds(childNodes); + + // Calculate parent node size with minimum width/height constraints + const width = Math.max(rect.width + 80, 840); + const height = Math.max(rect.height + 80, 600); // Update parentNode size and position onChangeNode({ @@ -317,14 +332,17 @@ export const useWorkflow = () => { } }); + // Calculate position offset + const offsetHeight = loopNode?.height ? loopNode.height - height - 380 : 0; + // Update parentNode position onNodesChange([ { id: parentId, type: 'position', position: { - x: rect.x - 50, - y: rect.y - 300 + x: rect.x - 70, + y: rect.y - (320 + offsetHeight) } } ]); @@ -335,43 +353,41 @@ export const useWorkflow = () => { const [helperLineVertical, setHelperLineVertical] = useState(); const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => { - requestAnimationFrame(() => { - const positionChange = change.type === 'position' && change.dragging ? change : undefined; + const positionChange = change.type === 'position' && change.dragging ? change : undefined; - if (positionChange?.position) { - // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 - const filterNodes = nodes - .filter((node) => { - if (!positionChange.position) return false; + if (positionChange?.position) { + // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 + const filterNodes = nodes + .filter((node) => { + if (!positionChange.position) return false; - return ( - Math.abs(node.position.x - positionChange.position.x) <= 3000 && - Math.abs(node.position.y - positionChange.position.y) <= 3000 - ); - }) - .sort((a, b) => { - if (!positionChange.position) return 0; - return ( - Math.abs(a.position.x - positionChange.position.x) + - Math.abs(a.position.y - positionChange.position.y) - - Math.abs(b.position.x - positionChange.position.x) - - Math.abs(b.position.y - positionChange.position.y) - ); - }) - .slice(0, 15); + return ( + Math.abs(node.position.x - positionChange.position.x) <= 3000 && + Math.abs(node.position.y - positionChange.position.y) <= 3000 + ); + }) + .sort((a, b) => { + if (!positionChange.position) return 0; + return ( + Math.abs(a.position.x - positionChange.position.x) + + Math.abs(a.position.y - positionChange.position.y) - + Math.abs(b.position.x - positionChange.position.x) - + Math.abs(b.position.y - positionChange.position.y) + ); + }) + .slice(0, 15); - const helperLines = computeHelperLines(positionChange, filterNodes); + const helperLines = computeHelperLines(positionChange, filterNodes); - positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x; - positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y; + positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x; + positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y; - setHelperLineHorizontal(helperLines.horizontal); - setHelperLineVertical(helperLines.vertical); - } else { - setHelperLineHorizontal(undefined); - setHelperLineVertical(undefined); - } - }); + setHelperLineHorizontal(helperLines.horizontal); + setHelperLineVertical(helperLines.vertical); + } else { + setHelperLineHorizontal(undefined); + setHelperLineVertical(undefined); + } }); // Check if a node is placed on top of a loop node @@ -412,9 +428,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), node]; - const rect = getNodesBounds(childNodes); - resetParentNodeSizeAndPosition(rect, parentNode.id); + resetParentNodeSizeAndPosition(parentNode.id); } }); @@ -469,7 +483,7 @@ export const useWorkflow = () => { const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId); checkNodeHelpLine(change, childNodes); - resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId); + resetParentNodeSizeAndPosition(parentId); } // If node is parent node, move parent node and child nodes else if (parentNode[node.data.flowNodeType]) { @@ -679,7 +693,8 @@ export const useWorkflow = () => { helperLineVertical, onNodeDragStop, onPaneContextMenu, - onPaneClick + onPaneClick, + resetParentNodeSizeAndPosition }; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 8a92cb29a..95d9bdadf 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -5,7 +5,7 @@ */ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef, useCallback } from 'react'; import { Background, NodeProps } from 'reactflow'; import NodeCard from '../render/NodeCard'; import Container from '../../components/Container'; @@ -24,17 +24,18 @@ import { import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../../context'; -import { cloneDeep } from 'lodash'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; import { AppContext } from '../../../../context'; import { isReferenceValue, isReferenceValueArray } from '@fastgpt/global/core/workflow/utils'; import { ReferenceItemValueType } from '@fastgpt/global/core/workflow/type/io'; +import { useWorkflow } from '../../hooks/useWorkflow'; const NodeLoop = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { nodeId, inputs, outputs, isFolded } = data; const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v); + const { resetParentNodeSizeAndPosition } = useWorkflow(); const loopInputArray = inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray); @@ -44,6 +45,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value }; }, [inputs]); + const childrenNodeIdList = useMemo(() => { return JSON.stringify( nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId) @@ -84,6 +86,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { return type ?? WorkflowIOValueTypeEnum.arrayAny; }, [appDetail.chatConfig, loopInputArray, nodeList]); + useEffect(() => { if (!loopInputArray) return; onChangeNode({ @@ -110,20 +113,15 @@ const NodeLoop = ({ data, selected }: NodeProps) => { }); }, [childrenNodeIdList, nodeId, onChangeNode]); + useEffect(() => { + setTimeout(() => { + resetParentNodeSizeAndPosition(nodeId); + }, 0); + }, [loopInputArray, nodeId, resetParentNodeSizeAndPosition]); + const Render = useMemo(() => { return ( - + @@ -132,7 +130,17 @@ const NodeLoop = ({ data, selected }: NodeProps) => { {t('workflow:loop_body')} - + diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx index 8a133ce30..cb5daaa70 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/DynamicInputs/index.tsx @@ -145,14 +145,25 @@ function Reference({ const onUpdateField = useCallback( ({ data }: { data: FlowNodeInputItemType }) => { if (!data.key) return; + const oldType = inputChildren.valueType; + const newType = data.valueType; + let newValue = data.value; + if (oldType?.includes('array') && !newType?.includes('array')) { + newValue = data.value[0]; + } else if (!oldType?.includes('array') && newType?.includes('array')) { + newValue = [data.value]; + } onChangeNode({ nodeId, type: 'replaceInput', key: inputChildren.key, value: { - ...data, - value: data + ...inputChildren, + value: newValue, + key: data.key, + label: data.label, + valueType: data.valueType } }); }, 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 f6e5389e6..d81d1218b 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 @@ -2,7 +2,10 @@ import React, { useCallback, useMemo } from 'react'; import type { RenderInputProps } from '../type'; import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import { computedNodeInputReference } from '@/web/core/workflow/utils'; +import { + computedNodeInputReference, + filterWorkflowNodeOutputsByType +} from '@/web/core/workflow/utils'; import { useTranslation } from 'next-i18next'; import { NodeOutputKeyEnum, @@ -77,7 +80,6 @@ export const useReference = ({ if (!sourceNodes) return []; const isArray = valueType?.includes('array'); - const arrayItemType = isArray ? valueType.replace('array', '').toLowerCase() : valueType; // 转换为 select 的数据结构 const list: CommonSelectProps['list'] = sourceNodes @@ -90,16 +92,7 @@ export const useReference = ({ ), value: node.nodeId, - children: node.outputs - .filter( - (output) => - valueType === WorkflowIOValueTypeEnum.any || - valueType === WorkflowIOValueTypeEnum.arrayAny || - output.valueType === WorkflowIOValueTypeEnum.any || - output.valueType === valueType || - // Array can select string - arrayItemType === output.valueType - ) + children: filterWorkflowNodeOutputsByType(node.outputs, valueType) .filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam) .map((output) => { return { @@ -199,7 +192,7 @@ const SingleReferenceSelector = ({ label={ isValidSelect ? ( - + {nodeName} {outputName} @@ -249,20 +242,14 @@ const MultipleReferenceSelector = ({ const ArraySelector = useMemo(() => { const selectorVal = value as ReferenceItemValueType[]; - const notValidItem = - !selectorVal || - selectorVal.length === 0 || - selectorVal.every((item) => { - const [nodeName, outputName] = getSelectValue(item); - return !nodeName || !outputName; - }); + const validSelectValue = selectorVal && selectorVal.length > 0; return ( - {selectorVal.map((item, index) => { + {selectorVal?.map((item, index) => { const [nodeName, outputName] = getSelectValue(item); const isInvalidItem = !nodeName || !outputName; @@ -270,8 +257,8 @@ const MultipleReferenceSelector = ({ - {nodeName} - - {outputName} + {isInvalidItem ? ( + <>{t('common:invalid_variable')} + ) : ( + <> + {nodeName} + + {outputName} + + )} ); - }, [getSelectValue, list, onSelect, placeholder, popDirection, value]); + }, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]); return ArraySelector; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index a6cf006f2..f67a96a0f 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -355,7 +355,6 @@ const WorkflowContextProvider = ({ const getNodes = useContextSelector(WorkflowActionContext, (state) => state.getNodes); const nodeListString = useContextSelector(WorkflowActionContext, (state) => state.nodeListString); - console.log(121211111111); const nodeList = useMemo( () => JSON.parse(nodeListString) as FlowNodeItemType[], [nodeListString] diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index b8878de8f..f07514aa3 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -24,7 +24,7 @@ import { getAppChatConfig, getGuideModule, isReferenceValue, - isReferenceValueArray + isReferenceValueFormat } from '@fastgpt/global/core/workflow/utils'; import { TFunction } from 'next-i18next'; import { @@ -263,6 +263,20 @@ export const getRefData = ({ }; }; +export const filterWorkflowNodeOutputsByType = ( + outputs: FlowNodeOutputItemType[], + valueType: WorkflowIOValueTypeEnum +): FlowNodeOutputItemType[] => { + return outputs.filter( + (output) => + valueType === WorkflowIOValueTypeEnum.any || + valueType === WorkflowIOValueTypeEnum.arrayAny || + output.valueType === WorkflowIOValueTypeEnum.any || + output.valueType === valueType || + valueType?.replace('array', '').toLowerCase() === output.valueType + ); +}; + /* Connection rules */ export const checkWorkflowNodeAndConnection = ({ nodes, @@ -337,7 +351,7 @@ export const checkWorkflowNodeAndConnection = ({ // check reference invalid const renderType = input.renderTypeList[input.selectedTypeIndex || 0]; - if (renderType === FlowNodeInputTypeEnum.reference && input.required) { + if (renderType === FlowNodeInputTypeEnum.reference) { const checkReference = (value: [string, string]) => { if (value[0] === VARIABLE_NODE_ID) { return false; @@ -348,18 +362,16 @@ export const checkWorkflowNodeAndConnection = ({ return true; } - const sourceOutput = sourceNode.data.outputs.find((item) => item.id === value[1]); + const sourceOutput = filterWorkflowNodeOutputsByType( + sourceNode.data.outputs, + input.valueType as WorkflowIOValueTypeEnum + ).find((item) => item.id === value[1]); return !sourceOutput; }; // Old format - if ( - Array.isArray(input.value) && - input.value.length === 2 && - typeof input.value[0] === 'string' && - typeof input.value[1] === 'string' - ) { - return checkReference(input.value as [string, string]); + if (isReferenceValueFormat(input.value)) { + return input.required && checkReference(input.value); } // New format