From 44a688cb81cbb546db9a2e493bfd8b049b3f5204 Mon Sep 17 00:00:00 2001 From: twwu Date: Tue, 30 Dec 2025 16:05:33 +0800 Subject: [PATCH] feat: implement edge source handle change functionality and enhance node interactions for HumanInput node --- .../workflow/hooks/use-edges-interactions.ts | 54 +++++++++++++++++++ .../workflow/hooks/use-nodes-interactions.ts | 4 ++ .../workflow/hooks/use-workflow-history.ts | 1 + .../use-workflow-node-finished.ts | 1 + .../nodes/_base/components/variable/utils.ts | 3 ++ .../nodes/human-input/hooks/use-config.ts | 14 +++++ .../components/workflow/utils/elk-layout.ts | 1 + .../components/nodes/base.tsx | 2 +- .../components/nodes/constants.ts | 1 + 9 files changed, 80 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/hooks/use-edges-interactions.ts b/web/app/components/workflow/hooks/use-edges-interactions.ts index 5104b47ef4..0e911c3de8 100644 --- a/web/app/components/workflow/hooks/use-edges-interactions.ts +++ b/web/app/components/workflow/hooks/use-edges-interactions.ts @@ -151,11 +151,65 @@ export const useEdgesInteractions = () => { setEdges(newEdges) }, [store, getNodesReadOnly]) + const handleEdgeSourceHandleChange = useCallback((nodeId: string, oldHandleId: string, newHandleId: string) => { + if (getNodesReadOnly()) + return + + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + + // Find edges connected to the old handle + const affectedEdges = edges.filter( + edge => edge.source === nodeId && edge.sourceHandle === oldHandleId, + ) + + if (affectedEdges.length === 0) + return + + // Update node metadata: remove old handle, add new handle + const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( + [ + ...affectedEdges.map(edge => ({ type: 'remove', edge })), + ...affectedEdges.map(edge => ({ + type: 'add', + edge: { ...edge, sourceHandle: newHandleId }, + })), + ], + nodes, + ) + + const newNodes = produce(nodes, (draft: Node[]) => { + draft.forEach((node) => { + if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { + node.data = { + ...node.data, + ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], + } + } + }) + }) + setNodes(newNodes) + + // Update edges to use new sourceHandle and regenerate edge IDs + const newEdges = produce(edges, (draft) => { + draft.forEach((edge) => { + if (edge.source === nodeId && edge.sourceHandle === oldHandleId) { + edge.sourceHandle = newHandleId + edge.id = `${edge.source}-${newHandleId}-${edge.target}-${edge.targetHandle}` + } + }) + }) + setEdges(newEdges) + handleSyncWorkflowDraft() + saveStateToHistory(WorkflowHistoryEvent.EdgeSourceHandleChange) + }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) + return { handleEdgeEnter, handleEdgeLeave, handleEdgeDeleteByDeleteBranch, handleEdgeDelete, handleEdgesChange, + handleEdgeSourceHandleChange, } } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index afb47d5994..d54b62c143 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -299,6 +299,7 @@ export const useNodesInteractions = () => { || connectingNode.data.type === BlockEnum.VariableAggregator) && node.data.type !== BlockEnum.IfElse && node.data.type !== BlockEnum.QuestionClassifier + && node.data.type !== BlockEnum.HumanInput ) { n.data._isEntering = true } @@ -1017,6 +1018,7 @@ export const useNodesInteractions = () => { if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier + && nodeType !== BlockEnum.HumanInput ) { newNode.data._connectedSourceHandleIds = [sourceHandle] } @@ -1053,6 +1055,7 @@ export const useNodesInteractions = () => { if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier + && nodeType !== BlockEnum.HumanInput && nodeType !== BlockEnum.LoopEnd ) { newEdge = { @@ -1244,6 +1247,7 @@ export const useNodesInteractions = () => { if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier + && nodeType !== BlockEnum.HumanInput && nodeType !== BlockEnum.LoopEnd ) { newNextEdge = { diff --git a/web/app/components/workflow/hooks/use-workflow-history.ts b/web/app/components/workflow/hooks/use-workflow-history.ts index 17270bea63..7feaec9709 100644 --- a/web/app/components/workflow/hooks/use-workflow-history.ts +++ b/web/app/components/workflow/hooks/use-workflow-history.ts @@ -27,6 +27,7 @@ export const WorkflowHistoryEvent = { NodeDelete: 'NodeDelete', EdgeDelete: 'EdgeDelete', EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch', + EdgeSourceHandleChange: 'EdgeSourceHandleChange', NodeAdd: 'NodeAdd', NodeResize: 'NodeResize', NoteAdd: 'NoteAdd', diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-finished.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-finished.ts index cf0d9bcef1..75e89414f7 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-finished.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-finished.ts @@ -49,6 +49,7 @@ export const useWorkflowNodeFinished = () => { if (data.node_type === BlockEnum.QuestionClassifier) currentNode.data._runningBranchId = data?.outputs?.class_id + // todo: add human-input node support } }) setNodes(newNodes) diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index a71894e685..dac6ae2e51 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -1309,6 +1309,7 @@ const replaceOldVarInText = ( ) } +// todo: add human-input node support export const getNodeUsedVars = (node: Node): ValueSelector[] => { const { data } = node const { type } = data @@ -1505,6 +1506,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => { } // can be used in iteration node +// todo: add human-input node export const getNodeUsedVarPassToServerKey = ( node: Node, valueSelector: ValueSelector, @@ -1615,6 +1617,7 @@ export const findUsedVarNodes = ( return res } +// todo: add human-input node export const updateNodeVars = ( oldNode: Node, oldVarSelector: ValueSelector, diff --git a/web/app/components/workflow/nodes/human-input/hooks/use-config.ts b/web/app/components/workflow/nodes/human-input/hooks/use-config.ts index b980491ca0..dc87cadf1e 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/use-config.ts @@ -1,16 +1,20 @@ import type { DeliveryMethod, HumanInputNodeType, UserAction } from '../types' import { produce } from 'immer' import { useState } from 'react' +import { useUpdateNodeInternals } from 'reactflow' import { useNodesReadOnly, } from '@/app/components/workflow/hooks' +import { useEdgesInteractions } from '@/app/components/workflow/hooks/use-edges-interactions' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import useFormContent from './use-form-content' const useConfig = (id: string, payload: HumanInputNodeType) => { + const updateNodeInternals = useUpdateNodeInternals() const { nodesReadOnly: readOnly } = useNodesReadOnly() const { inputs, setInputs } = useNodeCrud(id, payload) const formContentHook = useFormContent(id, payload) + const { handleEdgeDeleteByDeleteBranch, handleEdgeSourceHandleChange } = useEdgesInteractions() const [structuredOutputCollapsed, setStructuredOutputCollapsed] = useState(true) const handleDeliveryMethodChange = (methods: DeliveryMethod[]) => { @@ -36,6 +40,14 @@ const useConfig = (id: string, payload: HumanInputNodeType) => { ...inputs, user_actions: newActions, }) + + // Update edges to use the new handle + const oldAction = inputs.user_actions[index] + + if (oldAction && oldAction.id !== updatedAction.id) { + handleEdgeSourceHandleChange(id, oldAction.id, updatedAction.id) + updateNodeInternals(id) // Update handles + } } const handleUserActionDelete = (actionId: string) => { @@ -44,6 +56,8 @@ const useConfig = (id: string, payload: HumanInputNodeType) => { ...inputs, user_actions: newActions, }) + // Delete edges connected to this action + handleEdgeDeleteByDeleteBranch(id, actionId) } const handleTimeoutChange = ({ timeout, unit }: { timeout: number, unit: 'hour' | 'day' }) => { diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 1a4bbf2d50..e11d397871 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -389,6 +389,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]) const edgeToPortMap = new Map() // Build nodes with ports for If/Else nodes + // todo: add human-input node support nodes.forEach((node) => { if (node.data.type === BlockEnum.IfElse) { const portsResult = buildIfElseWithPorts(node, edges) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/base.tsx b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx index d75964b525..53a3fb5591 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/base.tsx +++ b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx @@ -68,7 +68,7 @@ const BaseCard = ({ handleId="target" /> { - data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && ( + data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && ( = { [BlockEnum.QuestionClassifier]: QuestionClassifierNode, [BlockEnum.IfElse]: IfElseNode,