diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 09548ffeaf..280d0f7b1d 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -1,4 +1,5 @@ import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api' +import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types' import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types' import type { Edge, @@ -345,6 +346,76 @@ const buildIfElseWithPorts = ( } } +/** + * Build Human Input node with ELK native Ports for multiple branches + * Handles user actions as branches with __timeout as the last fixed branch + */ +const buildHumanInputWithPorts = ( + humanInputNode: Node, + edges: Edge[], +): { node: ElkNodeShape, portMap: Map } | null => { + const childEdges = edges.filter(edge => edge.source === humanInputNode.id) + + if (childEdges.length <= 1) + return null + + // Sort child edges: user actions first (by order), then __timeout last + const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => { + const handleA = edgeA.sourceHandle + const handleB = edgeB.sourceHandle + + if (handleA && handleB) { + const userActions = (humanInputNode.data as HumanInputNodeType).user_actions || [] + const isATimeout = handleA === '__timeout' + const isBTimeout = handleB === '__timeout' + + // __timeout should always be last + if (isATimeout) + return 1 + if (isBTimeout) + return -1 + + // Sort by user_actions order + const indexA = userActions.findIndex(action => action.id === handleA) + const indexB = userActions.findIndex(action => action.id === handleB) + + if (indexA !== -1 && indexB !== -1) + return indexA - indexB + } + + return 0 + }) + + // Create ELK ports for each branch + const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({ + id: `${humanInputNode.id}-port-${edge.sourceHandle || index}`, + layoutOptions: { + 'port.side': 'EAST', + 'port.index': String(index), + }, + })) + + // Build port mapping: edge.id -> portId + const portMap = new Map() + sortedChildEdges.forEach((edge, index) => { + const portId = `${humanInputNode.id}-port-${edge.sourceHandle || index}` + portMap.set(edge.id, portId) + }) + + return { + node: { + id: humanInputNode.id, + width: humanInputNode.width ?? DEFAULT_NODE_WIDTH, + height: humanInputNode.height ?? DEFAULT_NODE_HEIGHT, + ports, + layoutOptions: { + 'elk.portConstraints': 'FIXED_ORDER', + }, + }, + portMap, + } +} + const normaliseBounds = (layout: LayoutResult): LayoutResult => { const { nodes, @@ -388,8 +459,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]) // Track which edges have been processed for If/Else nodes with ports const edgeToPortMap = new Map() - // Build nodes with ports for If/Else nodes - // todo: add human-input node support + // Build nodes with ports for If/Else and Human Input nodes nodes.forEach((node) => { if (node.data.type === BlockEnum.IfElse) { const portsResult = buildIfElseWithPorts(node, edges) @@ -406,6 +476,21 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]) elkNodes.push(toElkNode(node)) } } + else if (node.data.type === BlockEnum.HumanInput) { + const portsResult = buildHumanInputWithPorts(node, edges) + if (portsResult) { + // Use node with ports + elkNodes.push(portsResult.node) + // Store port mappings for edges + portsResult.portMap.forEach((portId, edgeId) => { + edgeToPortMap.set(edgeId, portId) + }) + } + else { + // No multiple branches, use normal node + elkNodes.push(toElkNode(node)) + } + } else { elkNodes.push(toElkNode(node)) }