import type { MouseEvent } from 'react' import type { NodeDragHandler, NodeMouseHandler, OnConnect, OnConnectEnd, OnConnectStart, ResizeParamsWithDirection, } from 'reactflow' import type { BlockDefaultValue } from '../block-selector/types' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types' import type { Edge, Node, OnNodeAdd } from '../types' import type { RAGPipelineVariables } from '@/models/pipeline' import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { getConnectedEdges, getOutgoers, useReactFlow } from 'reactflow' import { consoleQuery } from '@/service/client' import { collaborationManager } from '../collaboration/core/collaboration-manager' import { CUSTOM_EDGE, ITERATION_PADDING, LOOP_PADDING, NESTED_ELEMENT_Z_INDEX, NODE_WIDTH_X_OFFSET, X_OFFSET, Y_OFFSET, } from '../constants' import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' import { useCreateInlineAgentBinding } from '../nodes/agent-v2/hooks' import { isAgentV2NodeData, needsInlineAgentBindingCreation } from '../nodes/agent-v2/types' import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants' import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' import { useNodeLoopInteractions } from '../nodes/loop/use-interactions' import { CUSTOM_NOTE_NODE } from '../note-node/constants' import { useWorkflowStore } from '../store' import { BlockEnum, ControlMode, isTriggerNode } from '../types' import { generateNewNode, genNewNodeTitleFromOld, getNestedNodePosition, getNodeCatalogType, getNodeCustomTypeByNodeDataType, getNodesConnectedSourceOrTargetHandleIdsMap, getNodesWithSameDefaultDataType, getTopLeftNodePosition, isClipboardEdgeStructurallyValid, isClipboardNodeStructurallyValid, isClipboardValueCompatibleWithDefault, readWorkflowClipboard, sanitizeClipboardValueByDefault, writeWorkflowClipboard, } from '../utils' import { useWorkflowHistoryStore } from '../workflow-history-store' import { useAutoGenerateWebhookUrl } from './use-auto-generate-webhook-url' import { useCollaborativeWorkflow } from './use-collaborative-workflow' import { useHelpline } from './use-helpline' import useInspectVarsCrud from './use-inspect-vars-crud' import { useNodesMetaData } from './use-nodes-meta-data' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useNodesReadOnly, useWorkflow, useWorkflowReadOnly } from './use-workflow' import { useWorkflowHistory, WorkflowHistoryEvent } from './use-workflow-history' // Entry node deletion restriction has been removed to allow empty workflows // Entry node (Start/Trigger) wrapper offsets for alignment // Must match the values in use-helpline.ts const ENTRY_NODE_WRAPPER_OFFSET = { x: 0, y: 21, // Adjusted based on visual testing feedback } as const function needsPendingInlineAgentBinding(defaultValue?: unknown) { if (!defaultValue || typeof defaultValue !== 'object' || !('agent_binding' in defaultValue)) return false const binding = ( defaultValue as { agent_binding?: { agent_id?: string binding_type?: string current_snapshot_id?: string } } ).agent_binding return ( binding?.binding_type === 'inline_agent' && (!binding.agent_id || !binding.current_snapshot_id) ) } function agentV2NodeDefaultsNeedInlineBinding( defaultValue?: unknown, pluginDefaultValue?: unknown, ) { return needsPendingInlineAgentBinding({ ...(defaultValue && typeof defaultValue === 'object' ? defaultValue : {}), ...(pluginDefaultValue && typeof pluginDefaultValue === 'object' ? pluginDefaultValue : {}), }) } const pruneClipboardNodesWithFilteredAncestors = ( sourceNodes: Node[], candidateNodes: Node[], ): Node[] => { const candidateNodeIds = new Set(candidateNodes.map((node) => node.id)) const filteredRootIds = sourceNodes .filter((node) => !candidateNodeIds.has(node.id)) .map((node) => node.id) if (!filteredRootIds.length) return candidateNodes const childrenByParent = new Map() sourceNodes.forEach((node) => { if (!node.parentId) return const children = childrenByParent.get(node.parentId) ?? [] children.push(node.id) childrenByParent.set(node.parentId, children) }) const filteredNodeIds = new Set(filteredRootIds) const queue = [...filteredRootIds] while (queue.length) { const currentNodeId = queue.shift()! const children = childrenByParent.get(currentNodeId) ?? [] children.forEach((childId) => { if (filteredNodeIds.has(childId)) return filteredNodeIds.add(childId) queue.push(childId) }) } return candidateNodes.filter((node) => !filteredNodeIds.has(node.id)) } const getUniquePastedNodeTitle = (sourceTitle: string, reservedTitles: Set) => { let titleCandidate = sourceTitle while (reservedTitles.has(titleCandidate)) titleCandidate = genNewNodeTitleFromOld(titleCandidate) reservedTitles.add(titleCandidate) return titleCandidate } const isNoteLinkClickTarget = (target: EventTarget | null, node: Node) => { return ( node.type === CUSTOM_NOTE_NODE && target instanceof HTMLElement && !!target.closest('.note-editor-theme_link') ) } export const useNodesInteractions = () => { const { t } = useTranslation() const { data: appDslVersion = '' } = useQuery( consoleQuery.appDslVersion.get.queryOptions({ staleTime: Infinity, select: (data) => data.app_dsl_version, }), ) const collaborativeWorkflow = useCollaborativeWorkflow() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() const { store: workflowHistoryStore } = useWorkflowHistoryStore() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { getAfterNodesInSameBranch } = useWorkflow() const { getNodesReadOnly } = useNodesReadOnly() const { getWorkflowReadOnly } = useWorkflowReadOnly() const { handleSetHelpline } = useHelpline() const { handleNodeIterationChildDrag, handleNodeIterationChildrenCopy } = useNodeIterationInteractions() const { handleNodeLoopChildDrag, handleNodeLoopChildrenCopy } = useNodeLoopInteractions() const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number y: number }) const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const { saveStateToHistory, undo, redo } = useWorkflowHistory() const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl() const { createInlineAgentBinding } = useCreateInlineAgentBinding() const createInlineAgentBindingForNode = useCallback( ( nodeId: string, options?: { onError?: () => void }, ) => { workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId) handleSyncWorkflowDraft(true, true) createInlineAgentBinding(nodeId, { onError: () => { options?.onError?.() }, onSuccess: (binding) => { const { nodes, setNodes } = collaborativeWorkflow.getState() setNodes( produce(nodes, (draft) => { const node = draft.find((node) => node.id === nodeId) if (node) { if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data)) node.data.agent_binding = binding node.data._openInlineAgentPanel = true delete node.data._isTempNode } }), ) handleSyncWorkflowDraft(true, true) }, }) }, [collaborativeWorkflow, createInlineAgentBinding, handleSyncWorkflowDraft, workflowStore], ) const handleNodeDragStart = useCallback( (_, node) => { workflowStore.setState({ nodeAnimation: false }) if (getNodesReadOnly()) return if (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_NOTE_NODE) { return } if (node.type === CUSTOM_LOOP_START_NODE || node.type === CUSTOM_NOTE_NODE) { return } dragNodeStartPosition.current = { x: node.position.x, y: node.position.y, } }, [workflowStore, getNodesReadOnly], ) const handleNodeDrag = useCallback( (e, node: Node) => { if (getNodesReadOnly()) return if (node.type === CUSTOM_ITERATION_START_NODE) return if (node.type === CUSTOM_LOOP_START_NODE) return e.stopPropagation() const { nodes, setNodes } = collaborativeWorkflow.getState() const { restrictPosition } = handleNodeIterationChildDrag(node) const { restrictPosition: restrictLoopPosition } = handleNodeLoopChildDrag(node) const { showHorizontalHelpLineNodes, showVerticalHelpLineNodes } = handleSetHelpline(node) const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length const newNodes = produce(nodes, (draft) => { const currentNode = draft.find((n) => n.id === node.id)! // Check if current dragging node is an entry node const isCurrentEntryNode = isTriggerNode(node.data.type as BlockEnum) || node.data.type === BlockEnum.Start // X-axis alignment with offset consideration if (showVerticalHelpLineNodesLength > 0) { const targetNode = showVerticalHelpLineNodes[0] const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start // Calculate the wrapper position needed to align the inner nodes // Target inner position = target.position + target.offset // Current inner position should equal target inner position // So: current.position + current.offset = target.position + target.offset // Therefore: current.position = target.position + target.offset - current.offset const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0 const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0 currentNode.position.x = targetNode!.position.x + targetOffset - currentOffset } else if (restrictPosition.x !== undefined) { currentNode.position.x = restrictPosition.x } else if (restrictLoopPosition.x !== undefined) { currentNode.position.x = restrictLoopPosition.x } else { currentNode.position.x = node.position.x } // Y-axis alignment with offset consideration if (showHorizontalHelpLineNodesLength > 0) { const targetNode = showHorizontalHelpLineNodes[0] const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0 const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0 currentNode.position.y = targetNode!.position.y + targetOffset - currentOffset } else if (restrictPosition.y !== undefined) { currentNode.position.y = restrictPosition.y } else if (restrictLoopPosition.y !== undefined) { currentNode.position.y = restrictLoopPosition.y } else { currentNode.position.y = node.position.y } }) setNodes(newNodes) }, [ getNodesReadOnly, collaborativeWorkflow, handleNodeIterationChildDrag, handleNodeLoopChildDrag, handleSetHelpline, ], ) const handleNodeDragStop = useCallback( (_, node) => { const { setHelpLineHorizontal, setHelpLineVertical } = workflowStore.getState() if (getNodesReadOnly()) return const { x, y } = dragNodeStartPosition.current if (!(x === node.position.x && y === node.position.y)) { setHelpLineHorizontal() setHelpLineVertical() handleSyncWorkflowDraft() if (x !== 0 && y !== 0) { // selecting a note will trigger a drag stop event with x and y as 0 saveStateToHistory(WorkflowHistoryEvent.NodeDragStop, { nodeId: node.id, }) } } }, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft], ) const handleNodeEnter = useCallback( (_, node) => { if (getNodesReadOnly()) return if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE) { return } if (node.type === CUSTOM_LOOP_START_NODE || node.type === CUSTOM_NOTE_NODE) { return } const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState() const { connectingNodePayload, setEnteringNodePayload } = workflowStore.getState() if (connectingNodePayload) { if (connectingNodePayload.nodeId === node.id) return const connectingNode: Node = nodes.find((n) => n.id === connectingNodePayload.nodeId)! const sameLevel = connectingNode.parentId === node.parentId if (sameLevel) { setEnteringNodePayload({ nodeId: node.id, nodeData: node.data as VariableAssignerNodeType, }) const fromType = connectingNodePayload.handleType const newNodes = produce(nodes, (draft) => { draft.forEach((n) => { if ( n.id === node.id && fromType === 'source' && (node.data.type === BlockEnum.VariableAssigner || node.data.type === BlockEnum.VariableAggregator) ) { if (!node.data.advanced_settings?.group_enabled) n.data._isEntering = true } if ( n.id === node.id && fromType === 'target' && (connectingNode.data.type === BlockEnum.VariableAssigner || connectingNode.data.type === BlockEnum.VariableAggregator) && node.data.type !== BlockEnum.IfElse && node.data.type !== BlockEnum.QuestionClassifier && node.data.type !== BlockEnum.HumanInput ) { n.data._isEntering = true } }) }) setNodes(newNodes, false) } } const newEdges = produce(edges, (draft) => { const connectedEdges = getConnectedEdges([node], edges) connectedEdges.forEach((edge) => { const currentEdge = draft.find((e) => e.id === edge.id) if (currentEdge) currentEdge.data._connectedNodeIsHovering = true }) }) setEdges(newEdges, false) }, [collaborativeWorkflow, workflowStore, getNodesReadOnly], ) const handleNodeLeave = useCallback( (_, node) => { if (getNodesReadOnly()) return if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE) { return } if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_LOOP_START_NODE) { return } const { setEnteringNodePayload } = workflowStore.getState() setEnteringNodePayload(undefined) const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { node.data._isEntering = false }) }) setNodes(newNodes, false) const newEdges = produce(edges, (draft) => { draft.forEach((edge) => { edge.data._connectedNodeIsHovering = false }) }) setEdges(newEdges, false) }, [collaborativeWorkflow, workflowStore, getNodesReadOnly], ) const handleNodeSelect = useCallback( (nodeId: string, cancelSelection?: boolean, initShowLastRunTab?: boolean) => { if (initShowLastRunTab) workflowStore.setState({ initShowLastRunTab: true }) const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { const selected = node.id === nodeId && !cancelSelection node.selected = selected node.data.selected = selected }) }) setNodes(newNodes, false) const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).map( (edge) => edge.id, ) const newEdges = produce(edges, (draft) => { draft.forEach((edge) => { if (connectedEdges.includes(edge.id)) { edge.data = { ...edge.data, _connectedNodeIsSelected: !cancelSelection, } } else { edge.data = { ...edge.data, _connectedNodeIsSelected: false, } } }) }) setEdges(newEdges, false) }, [collaborativeWorkflow], ) const handleNodeClick = useCallback( (event, node) => { const { controlMode } = workflowStore.getState() if (controlMode === ControlMode.Comment) return if (isNoteLinkClickTarget(event.target, node)) return if (node.type === CUSTOM_ITERATION_START_NODE) return if (node.type === CUSTOM_LOOP_START_NODE) return if (node.data.type === BlockEnum.DataSourceEmpty) return handleNodeSelect(node.id) }, [handleNodeSelect, workflowStore], ) const handleNodeConnect = useCallback( ({ source, sourceHandle, target, targetHandle }) => { if (source === target) return if (getNodesReadOnly()) return const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState() const targetNode = nodes.find((node) => node.id === target!) const sourceNode = nodes.find((node) => node.id === source!) if (targetNode?.parentId !== sourceNode?.parentId) return if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE) { return } if ( edges.some( (edge) => edge.source === source && edge.sourceHandle === sourceHandle && edge.target === target && edge.targetHandle === targetHandle, ) ) { return } const parendNode = nodes.find((node) => node.id === targetNode?.parentId) const isInIteration = parendNode && parendNode.data.type === BlockEnum.Iteration const isInLoop = !!parendNode && parendNode.data.type === BlockEnum.Loop const newEdge = { id: `${source}-${sourceHandle}-${target}-${targetHandle}`, type: CUSTOM_EDGE, source: source!, target: target!, sourceHandle, targetHandle, data: { sourceType: nodes.find((node) => node.id === source)!.data.type, targetType: nodes.find((node) => node.id === target)!.data.type, isInIteration, iteration_id: isInIteration ? targetNode?.parentId : undefined, isInLoop, loop_id: isInLoop ? targetNode?.parentId : undefined, }, zIndex: targetNode?.parentId ? NESTED_ELEMENT_Z_INDEX : 0, } const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( [{ type: 'add', edge: newEdge }], nodes, ) const newNodes = produce(nodes, (draft: Node[]) => { draft.forEach((node) => { if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } }) }) const newEdges = produce(edges, (draft) => { draft.push(newEdge) }) setNodes(newNodes) setEdges(newEdges) handleSyncWorkflowDraft() saveStateToHistory(WorkflowHistoryEvent.NodeConnect, { nodeId: targetNode?.id, }) }, [ getNodesReadOnly, collaborativeWorkflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, ], ) const handleNodeConnectStart = useCallback( (_, { nodeId, handleType, handleId }) => { if (getNodesReadOnly()) return if (nodeId && handleType) { const { setConnectingNodePayload } = workflowStore.getState() const { nodes } = collaborativeWorkflow.getState() const node = nodes.find((n) => n.id === nodeId)! if (node.type === CUSTOM_NOTE_NODE) return if ( node.data.type === BlockEnum.VariableAggregator || node.data.type === BlockEnum.VariableAssigner ) { if (handleType === 'target') return } setConnectingNodePayload({ nodeId, nodeType: node.data.type, handleType, handleId, }) } }, [collaborativeWorkflow, workflowStore, getNodesReadOnly], ) const handleNodeConnectEnd = useCallback( (e) => { if (getNodesReadOnly()) return const { connectingNodePayload, setConnectingNodePayload, enteringNodePayload, setEnteringNodePayload, } = workflowStore.getState() if (connectingNodePayload && enteringNodePayload) { const { setShowAssignVariablePopup, hoveringAssignVariableGroupId } = workflowStore.getState() const { screenToFlowPosition } = reactflow const { nodes, setNodes } = collaborativeWorkflow.getState() const fromHandleType = connectingNodePayload.handleType const fromHandleId = connectingNodePayload.handleId const fromNode = nodes.find((n) => n.id === connectingNodePayload.nodeId)! const toNode = nodes.find((n) => n.id === enteringNodePayload.nodeId)! const toParentNode = nodes.find((n) => n.id === toNode.parentId) if (fromNode.parentId !== toNode.parentId) return const pointer = e as { x: number; y: number } const { x, y } = screenToFlowPosition({ x: pointer.x, y: pointer.y }) if ( fromHandleType === 'source' && (toNode.data.type === BlockEnum.VariableAssigner || toNode.data.type === BlockEnum.VariableAggregator) ) { const groupEnabled = toNode.data.advanced_settings?.group_enabled const firstGroupId = toNode.data.advanced_settings?.groups[0].groupId let handleId = 'target' if (groupEnabled) { if (hoveringAssignVariableGroupId) handleId = hoveringAssignVariableGroupId else handleId = firstGroupId } const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { if (node.id === toNode.id) { node.data._showAddVariablePopup = true node.data._holdAddVariablePopup = true } }) }) setNodes(newNodes) setShowAssignVariablePopup({ nodeId: fromNode.id, nodeData: fromNode.data, variableAssignerNodeId: toNode.id, variableAssignerNodeData: toNode.data, variableAssignerNodeHandleId: handleId, parentNode: toParentNode, x: x - toNode.positionAbsolute!.x, y: y - toNode.positionAbsolute!.y, }) handleNodeConnect({ source: fromNode.id, sourceHandle: fromHandleId, target: toNode.id, targetHandle: 'target', }) } } setConnectingNodePayload(undefined) setEnteringNodePayload(undefined) }, [collaborativeWorkflow, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow], ) const { deleteNodeInspectorVars } = useInspectVarsCrud() const handleNodeDelete = useCallback( (nodeId: string) => { if (getNodesReadOnly()) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const currentNodeIndex = nodes.findIndex((node) => node.id === nodeId) const currentNode = nodes[currentNodeIndex] if (!currentNode) return if (nodesMetaDataMap?.[getNodeCatalogType(currentNode.data)]?.metaData.isUndeletable) { return } deleteNodeInspectorVars(nodeId) if (currentNode.data.type === BlockEnum.Iteration) { const iterationChildren = nodes.filter((node) => node.parentId === currentNode.id) if (iterationChildren.length) { if (currentNode.data._isBundled) { iterationChildren.forEach((child) => { handleNodeDelete(child.id) }) return handleNodeDelete(nodeId) } else { if (iterationChildren.length === 1) { handleNodeDelete(iterationChildren[0]!.id) handleNodeDelete(nodeId) return } const { setShowConfirm, showConfirm } = workflowStore.getState() if (!showConfirm) { setShowConfirm({ title: t(($) => $['nodes.iteration.deleteTitle'], { ns: 'workflow' }), desc: t(($) => $['nodes.iteration.deleteDesc'], { ns: 'workflow' }) || '', onConfirm: () => { iterationChildren.forEach((child) => { handleNodeDelete(child.id) }) handleNodeDelete(nodeId) handleSyncWorkflowDraft() setShowConfirm(undefined) }, }) return } } } } if (currentNode.data.type === BlockEnum.Loop) { const loopChildren = nodes.filter((node) => node.parentId === currentNode.id) if (loopChildren.length) { if (currentNode.data._isBundled) { loopChildren.forEach((child) => { handleNodeDelete(child.id) }) return handleNodeDelete(nodeId) } else { if (loopChildren.length === 1) { handleNodeDelete(loopChildren[0]!.id) handleNodeDelete(nodeId) return } const { setShowConfirm, showConfirm } = workflowStore.getState() if (!showConfirm) { setShowConfirm({ title: t(($) => $['nodes.loop.deleteTitle'], { ns: 'workflow' }), desc: t(($) => $['nodes.loop.deleteDesc'], { ns: 'workflow' }) || '', onConfirm: () => { loopChildren.forEach((child) => { handleNodeDelete(child.id) }) handleNodeDelete(nodeId) handleSyncWorkflowDraft() setShowConfirm(undefined) }, }) return } } } } if (currentNode.data.type === BlockEnum.DataSource) { const { id } = currentNode const { ragPipelineVariables, setRagPipelineVariables } = workflowStore.getState() if (ragPipelineVariables && setRagPipelineVariables) { const newRagPipelineVariables: RAGPipelineVariables = [] ragPipelineVariables.forEach((variable) => { if (variable.belong_to_node_id === id) return newRagPipelineVariables.push(variable) }) setRagPipelineVariables(newRagPipelineVariables) } } const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges) const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( connectedEdges.map((edge) => ({ type: 'remove', edge })), nodes, ) const newNodes = produce(nodes, (draft: Node[]) => { draft.forEach((node) => { if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } if (node.id === currentNode.parentId) { node.data._children = node.data._children?.filter((child) => child.nodeId !== nodeId) } }) draft.splice(currentNodeIndex, 1) }) setNodes(newNodes, true, 'nodes:perform-batch-cascade-delete') const newEdges = produce(edges, (draft) => { return draft.filter( (edge) => !connectedEdges.some((connectedEdge) => connectedEdge.id === edge.id), ) }) setEdges(newEdges) handleSyncWorkflowDraft() if (currentNode.type === CUSTOM_NOTE_NODE) { saveStateToHistory(WorkflowHistoryEvent.NoteDelete, { nodeId: currentNode.id, }) } else { saveStateToHistory(WorkflowHistoryEvent.NodeDelete, { nodeId: currentNode.id, }) } }, [ getNodesReadOnly, collaborativeWorkflow, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t, nodesMetaDataMap, deleteNodeInspectorVars, ], ) const handleNodeAdd = useCallback( ( { nodeType, sourceHandle = 'source', targetHandle = 'target', pluginDefaultValue }, { prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle }, ) => { if (getNodesReadOnly()) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const nodeMetaData = nodesMetaDataMap?.[nodeType] if (!nodeMetaData) return const { defaultValue } = nodeMetaData const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue) const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && agentV2NodeDefaultsNeedInlineBinding(defaultValue, pluginDefaultValue) const { newNode, newIterationStartNode, newLoopStartNode } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(nodeType), data: { ...(defaultValue as Node['data']), title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title, ...pluginDefaultValue, selected: true, ...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}), _showAddVariablePopup: (nodeType === BlockEnum.VariableAssigner || nodeType === BlockEnum.VariableAggregator) && !!prevNodeId, _holdAddVariablePopup: false, }, position: { x: 0, y: 0, }, }) if (prevNodeId && !nextNodeId) { const prevNodeIndex = nodes.findIndex((node) => node.id === prevNodeId) const prevNode = nodes[prevNodeIndex] const outgoers = getOutgoers(prevNode!, nodes, edges).sort( (a, b) => a.position.y - b.position.y, ) const lastOutgoer = outgoers.at(-1) newNode.data._connectedTargetHandleIds = nodeType === BlockEnum.DataSource ? [] : [targetHandle] newNode.data._connectedSourceHandleIds = [] newNode.position = { x: lastOutgoer ? lastOutgoer.position.x : prevNode!.position.x + prevNode!.width! + X_OFFSET, y: lastOutgoer ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET : prevNode!.position.y, } newNode.parentId = prevNode!.parentId newNode.extent = prevNode!.extent const parentNode = nodes.find((node) => node.id === prevNode!.parentId) || null const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop if (prevNode!.parentId) { newNode.data.isInIteration = isInIteration newNode.data.isInLoop = isInLoop if (isInIteration) { newNode.data.iteration_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } if (isInLoop) { newNode.data.loop_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } if ( isInIteration && (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner) ) { const iterNodeData: IterationNodeType = parentNode.data iterNodeData._isShowTips = true } if ( isInLoop && (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner) ) { const iterNodeData: IterationNodeType = parentNode.data iterNodeData._isShowTips = true } } let newEdge = null if (nodeType !== BlockEnum.DataSource) { newEdge = { id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`, type: CUSTOM_EDGE, source: prevNodeId, sourceHandle: prevNodeSourceHandle, target: newNode.id, targetHandle, data: { sourceType: prevNode!.data.type, targetType: newNode.data.type, isInIteration, isInLoop, iteration_id: isInIteration ? prevNode!.parentId : undefined, loop_id: isInLoop ? prevNode!.parentId : undefined, _connectedNodeIsSelected: true, }, zIndex: prevNode!.parentId ? NESTED_ELEMENT_Z_INDEX : 0, } } const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( newEdge ? [{ type: 'add', edge: newEdge }] : [], nodes, ) const newNodes = produce(nodes, (draft: Node[]) => { draft.forEach((node) => { node.data.selected = false if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } if (node.data.type === BlockEnum.Iteration && prevNode!.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } if (node.data.type === BlockEnum.Loop && prevNode!.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } }) draft.push(newNode) if (newIterationStartNode) draft.push(newIterationStartNode) if (newLoopStartNode) draft.push(newLoopStartNode) }) if ( newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator ) { const { setShowAssignVariablePopup } = workflowStore.getState() setShowAssignVariablePopup({ nodeId: prevNode!.id, nodeData: prevNode!.data, variableAssignerNodeId: newNode.id, variableAssignerNodeData: newNode.data as VariableAssignerNodeType, variableAssignerNodeHandleId: targetHandle, parentNode: nodes.find((node) => node.id === newNode.parentId), x: -25, y: 44, }) } const newEdges = produce(edges, (draft) => { draft.forEach((item) => { item.data = { ...item.data, _connectedNodeIsSelected: false, } }) if (newEdge) draft.push(newEdge) }) setNodes(newNodes) setEdges(newEdges) } if (!prevNodeId && nextNodeId) { const nextNodeIndex = nodes.findIndex((node) => node.id === nextNodeId) const nextNode = nodes[nextNodeIndex]! if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.HumanInput ) { newNode.data._connectedSourceHandleIds = [sourceHandle] } newNode.data._connectedTargetHandleIds = [] newNode.position = { x: nextNode.position.x, y: nextNode.position.y, } newNode.parentId = nextNode.parentId newNode.extent = nextNode.extent const parentNode = nodes.find((node) => node.id === nextNode.parentId) || null const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop if (parentNode && nextNode.parentId) { newNode.data.isInIteration = isInIteration newNode.data.isInLoop = isInLoop if (isInIteration) { newNode.data.iteration_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } if (isInLoop) { newNode.data.loop_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } } let newEdge if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.HumanInput && nodeType !== BlockEnum.LoopEnd ) { newEdge = { id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, type: CUSTOM_EDGE, source: newNode.id, sourceHandle, target: nextNodeId, targetHandle: nextNodeTargetHandle, data: { sourceType: newNode.data.type, targetType: nextNode.data.type, isInIteration, isInLoop, iteration_id: isInIteration ? nextNode.parentId : undefined, loop_id: isInLoop ? nextNode.parentId : undefined, _connectedNodeIsSelected: true, }, zIndex: nextNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0, } } let nodesConnectedSourceOrTargetHandleIdsMap: ReturnType< typeof getNodesConnectedSourceOrTargetHandleIdsMap > if (newEdge) { nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( [{ type: 'add', edge: newEdge }], nodes, ) } const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!) const afterNodesInSameBranchIds = afterNodesInSameBranch.map((node) => node.id) const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { node.data.selected = false if (afterNodesInSameBranchIds.includes(node.id)) node.position.x += NODE_WIDTH_X_OFFSET if (nodesConnectedSourceOrTargetHandleIdsMap?.[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) { node.data.start_node_id = newNode.id node.data.startNodeType = newNode.data.type } if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) { node.data.start_node_id = newNode.id node.data.startNodeType = newNode.data.type } }) draft.push(newNode) if (newIterationStartNode) draft.push(newIterationStartNode) if (newLoopStartNode) draft.push(newLoopStartNode) }) if (newEdge) { const newEdges = produce(edges, (draft) => { draft.forEach((item) => { item.data = { ...item.data, _connectedNodeIsSelected: false, } }) draft.push(newEdge) }) setNodes(newNodes) setEdges(newEdges) } else { setNodes(newNodes) } } if (prevNodeId && nextNodeId) { const prevNode = nodes.find((node) => node.id === prevNodeId)! const nextNode = nodes.find((node) => node.id === nextNodeId)! newNode.data._connectedTargetHandleIds = nodeType === BlockEnum.DataSource ? [] : [targetHandle] newNode.data._connectedSourceHandleIds = [sourceHandle] newNode.position = { x: nextNode.position.x, y: nextNode.position.y, } newNode.parentId = prevNode.parentId newNode.extent = prevNode.extent const parentNode = nodes.find((node) => node.id === prevNode.parentId) || null const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop if (parentNode && prevNode.parentId) { newNode.data.isInIteration = isInIteration newNode.data.isInLoop = isInLoop if (isInIteration) { newNode.data.iteration_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } if (isInLoop) { newNode.data.loop_id = parentNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX } } const currentEdgeIndex = edges.findIndex( (edge) => edge.source === prevNodeId && edge.target === nextNodeId, ) let newPrevEdge = null if (nodeType !== BlockEnum.DataSource) { newPrevEdge = { id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`, type: CUSTOM_EDGE, source: prevNodeId, sourceHandle: prevNodeSourceHandle, target: newNode.id, targetHandle, data: { sourceType: prevNode.data.type, targetType: newNode.data.type, isInIteration, isInLoop, iteration_id: isInIteration ? prevNode.parentId : undefined, loop_id: isInLoop ? prevNode.parentId : undefined, _connectedNodeIsSelected: true, }, zIndex: prevNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0, } } let newNextEdge: Edge | null = null const nextNodeParentNode = nodes.find((node) => node.id === nextNode.parentId) || null const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop if ( nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.HumanInput && nodeType !== BlockEnum.LoopEnd ) { newNextEdge = { id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, type: CUSTOM_EDGE, source: newNode.id, sourceHandle, target: nextNodeId, targetHandle: nextNodeTargetHandle, data: { sourceType: newNode.data.type, targetType: nextNode.data.type, isInIteration: isNextNodeInIteration, isInLoop: isNextNodeInLoop, iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined, loop_id: isNextNodeInLoop ? nextNode.parentId : undefined, _connectedNodeIsSelected: true, }, zIndex: nextNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0, } } const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( [ { type: 'remove', edge: edges[currentEdgeIndex]! }, ...(newPrevEdge ? [{ type: 'add', edge: newPrevEdge }] : []), ...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []), ], [...nodes, newNode], ) const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!) const afterNodesInSameBranchIds = afterNodesInSameBranch.map((node) => node.id) const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { node.data.selected = false if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } if (afterNodesInSameBranchIds.includes(node.id)) node.position.x += NODE_WIDTH_X_OFFSET if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id) { node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type, }) } }) draft.push(newNode) if (newIterationStartNode) draft.push(newIterationStartNode) if (newLoopStartNode) draft.push(newLoopStartNode) }) setNodes(newNodes) if ( newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator ) { const { setShowAssignVariablePopup } = workflowStore.getState() setShowAssignVariablePopup({ nodeId: prevNode.id, nodeData: prevNode.data, variableAssignerNodeId: newNode.id, variableAssignerNodeData: newNode.data as VariableAssignerNodeType, variableAssignerNodeHandleId: targetHandle, parentNode: nodes.find((node) => node.id === newNode.parentId), x: -25, y: 44, }) } const newEdges = produce(edges, (draft) => { draft.splice(currentEdgeIndex, 1) draft.forEach((item) => { item.data = { ...item.data, _connectedNodeIsSelected: false, } }) if (newPrevEdge) draft.push(newPrevEdge) if (newNextEdge) draft.push(newNextEdge) }) setEdges(newEdges) } if (isAgentV2NodeData(newNode.data) && needsInlineAgentBindingCreation(newNode.data)) { saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id }) createInlineAgentBindingForNode(newNode.id, { onError: () => { const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() setNodes(nodes.filter((node) => node.id !== newNode.id)) setEdges( edges.filter((edge) => edge.source !== newNode.id && edge.target !== newNode.id), ) }, }) return } handleSyncWorkflowDraft() saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id }) }, [ getNodesReadOnly, collaborativeWorkflow, handleSyncWorkflowDraft, createInlineAgentBindingForNode, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, nodesMetaDataMap, ], ) const handleNodeChange = useCallback( ( currentNodeId: string, nodeType: BlockEnum, sourceHandle: string, pluginDefaultValue?: BlockDefaultValue, ) => { if (getNodesReadOnly()) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const currentNode = nodes.find((node) => node.id === currentNodeId)! const connectedEdges = getConnectedEdges([currentNode], edges) const nodeMetaData = nodesMetaDataMap?.[nodeType] if (!nodeMetaData) return const { defaultValue } = nodeMetaData const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue) const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && agentV2NodeDefaultsNeedInlineBinding(defaultValue, pluginDefaultValue) const { newNode: newCurrentNode, newIterationStartNode, newLoopStartNode, } = generateNewNode({ type: getNodeCustomTypeByNodeDataType(nodeType), data: { ...(defaultValue as Node['data']), title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title, ...pluginDefaultValue, ...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}), _connectedSourceHandleIds: [], _connectedTargetHandleIds: [], selected: currentNode.data.selected, isInIteration: currentNode.data.isInIteration, isInLoop: currentNode.data.isInLoop, iteration_id: currentNode.data.iteration_id, loop_id: currentNode.data.loop_id, }, position: { x: currentNode.position.x, y: currentNode.position.y, }, parentId: currentNode.parentId, extent: currentNode.extent, zIndex: currentNode.zIndex, }) const parentNode = nodes.find((node) => node.id === currentNode.parentId) const newNodeIsInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration const newNodeIsInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop const outgoingEdges = connectedEdges.filter((edge) => edge.source === currentNodeId) const normalizedSourceHandle = sourceHandle || 'source' const outgoingHandles = new Set(outgoingEdges.map((edge) => edge.sourceHandle || 'source')) const branchSourceHandle = currentNode.data._targetBranches?.[0]?.id let outgoingHandleToPreserve = normalizedSourceHandle if (!outgoingHandles.has(outgoingHandleToPreserve)) { if (branchSourceHandle && outgoingHandles.has(branchSourceHandle)) outgoingHandleToPreserve = branchSourceHandle else if (outgoingHandles.has('source')) outgoingHandleToPreserve = 'source' else outgoingHandleToPreserve = outgoingEdges[0]?.sourceHandle || 'source' } const outgoingEdgesToPreserve = outgoingEdges.filter( (edge) => (edge.sourceHandle || 'source') === outgoingHandleToPreserve, ) const outgoingEdgeIds = new Set(outgoingEdgesToPreserve.map((edge) => edge.id)) const newNodeSourceHandle = newCurrentNode.data._targetBranches?.[0]?.id || 'source' const reconnectedEdges = connectedEdges.reduce((acc, edge) => { if (outgoingEdgeIds.has(edge.id)) { const originalTargetNode = nodes.find((node) => node.id === edge.target) const targetNodeForEdge = originalTargetNode && originalTargetNode.id !== currentNodeId ? originalTargetNode : newCurrentNode if (!targetNodeForEdge) return acc const targetHandle = edge.targetHandle || 'target' const targetParentNode = targetNodeForEdge.id === newCurrentNode.id ? parentNode || null : nodes.find((node) => node.id === targetNodeForEdge.parentId) || null const isInIteration = !!targetParentNode && targetParentNode.data.type === BlockEnum.Iteration const isInLoop = !!targetParentNode && targetParentNode.data.type === BlockEnum.Loop acc.push({ ...edge, id: `${newCurrentNode.id}-${newNodeSourceHandle}-${targetNodeForEdge.id}-${targetHandle}`, source: newCurrentNode.id, sourceHandle: newNodeSourceHandle, target: targetNodeForEdge.id, targetHandle, type: CUSTOM_EDGE, data: { ...edge.data, sourceType: newCurrentNode.data.type, targetType: targetNodeForEdge.data.type, isInIteration, iteration_id: isInIteration ? targetNodeForEdge.parentId : undefined, isInLoop, loop_id: isInLoop ? targetNodeForEdge.parentId : undefined, _connectedNodeIsSelected: false, }, zIndex: targetNodeForEdge.parentId ? NESTED_ELEMENT_Z_INDEX : 0, }) } if ( edge.target === currentNodeId && edge.source !== currentNodeId && !outgoingEdgeIds.has(edge.id) ) { const sourceNode = nodes.find((node) => node.id === edge.source) if (!sourceNode) return acc const targetHandle = edge.targetHandle || 'target' const sourceHandle = edge.sourceHandle || 'source' acc.push({ ...edge, id: `${sourceNode.id}-${sourceHandle}-${newCurrentNode.id}-${targetHandle}`, source: sourceNode.id, sourceHandle, target: newCurrentNode.id, targetHandle, type: CUSTOM_EDGE, data: { ...edge.data, sourceType: sourceNode.data.type, targetType: newCurrentNode.data.type, isInIteration: newNodeIsInIteration, iteration_id: newNodeIsInIteration ? newCurrentNode.parentId : undefined, isInLoop: newNodeIsInLoop, loop_id: newNodeIsInLoop ? newCurrentNode.parentId : undefined, _connectedNodeIsSelected: false, }, zIndex: newCurrentNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0, }) } return acc }, []) const nodesWithNewNode = produce(nodes, (draft) => { draft.forEach((node) => { node.data.selected = false }) const index = draft.findIndex((node) => node.id === currentNodeId) draft.splice(index, 1, newCurrentNode) if (newIterationStartNode) draft.push(newIterationStartNode) if (newLoopStartNode) draft.push(newLoopStartNode) }) const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( [ ...connectedEdges.map((edge) => ({ type: 'remove', edge })), ...reconnectedEdges.map((edge) => ({ type: 'add', edge })), ], nodesWithNewNode, ) const newNodes = produce(nodesWithNewNode, (draft) => { draft.forEach((node) => { if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } }) }) setNodes(newNodes) const remainingEdges = edges.filter( (edge) => !connectedEdges.some((connectedEdge) => connectedEdge.id === edge.id), ) setEdges([...remainingEdges, ...reconnectedEdges]) if (nodeType === BlockEnum.TriggerWebhook) { handleSyncWorkflowDraft(true, true, { onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id), }) } else if ( isAgentV2NodeData(newCurrentNode.data) && needsInlineAgentBindingCreation(newCurrentNode.data) ) { createInlineAgentBindingForNode(newCurrentNode.id, { onError: () => { const { setNodes, setEdges } = collaborativeWorkflow.getState() setNodes(nodes) setEdges(edges) }, }) } else { handleSyncWorkflowDraft() } saveStateToHistory(WorkflowHistoryEvent.NodeChange, { nodeId: currentNodeId, }) }, [ getNodesReadOnly, collaborativeWorkflow, handleSyncWorkflowDraft, createInlineAgentBindingForNode, saveStateToHistory, nodesMetaDataMap, autoGenerateWebhookUrl, ], ) const handleNodesCancelSelected = useCallback(() => { const { nodes, setNodes } = collaborativeWorkflow.getState() const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { node.data.selected = false }) }) setNodes(newNodes) }, [collaborativeWorkflow]) const handleNodeContextMenu = useCallback( (e: MouseEvent, node: Node) => { if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE) { e.stopPropagation() return } if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_LOOP_START_NODE) { e.stopPropagation() return } e.preventDefault() workflowStore.setState({ contextMenuTarget: { type: 'node', nodeId: node.id, }, }) handleNodeSelect(node.id, true) }, [workflowStore, handleNodeSelect], ) const isNodeCopyable = useCallback( (node: Node) => { if (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE) { return false } if ( node.data.type === BlockEnum.Start || node.data.type === BlockEnum.LoopEnd || node.data.type === BlockEnum.KnowledgeBase || node.data.type === BlockEnum.DataSourceEmpty ) { return false } if (node.type === CUSTOM_NOTE_NODE) return true const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)] if (!nodeMeta) return false const { metaData } = nodeMeta return !metaData.isSingleton }, [nodesMetaDataMap], ) const getNodeDefaultValueForPaste = useCallback( (node: Node) => { if (node.type === CUSTOM_NOTE_NODE) return {} const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)] return nodeMeta?.defaultValue }, [nodesMetaDataMap], ) const handleNodesCopy = useCallback( (nodeId?: string) => { if (getNodesReadOnly()) return const { setClipboardData } = workflowStore.getState() const { nodes, edges } = collaborativeWorkflow.getState() let nodesToCopy: Node[] = [] if (nodeId) { const nodeToCopy = nodes.find((node) => node.id === nodeId && isNodeCopyable(node)) if (nodeToCopy) nodesToCopy = [nodeToCopy] } else { const bundledNodes = nodes.filter((node) => { if (!node.data._isBundled) return false if (!isNodeCopyable(node)) return false if (node.type === CUSTOM_NOTE_NODE) return true return !node.data.isInIteration && !node.data.isInLoop }) if (bundledNodes.length) { nodesToCopy = bundledNodes } else { const selectedNodes = nodes.filter((node) => node.data.selected && isNodeCopyable(node)) if (selectedNodes.length) nodesToCopy = selectedNodes } } if (!nodesToCopy.length) return const copiedNodesMap = new Map(nodesToCopy.map((node) => [node.id, node])) const queue = nodesToCopy .filter( (node) => node.data.type === BlockEnum.Iteration || node.data.type === BlockEnum.Loop, ) .map((node) => node.id) while (queue.length) { const parentId = queue.shift()! nodes.forEach((node) => { if (node.parentId !== parentId || copiedNodesMap.has(node.id)) return copiedNodesMap.set(node.id, node) if (node.data.type === BlockEnum.Iteration || node.data.type === BlockEnum.Loop) queue.push(node.id) }) } const copiedNodes = [...copiedNodesMap.values()] const copiedNodeIds = new Set(copiedNodes.map((node) => node.id)) const copiedEdges = edges.filter( (edge) => copiedNodeIds.has(edge.source) && copiedNodeIds.has(edge.target), ) const clipboardData = { nodes: copiedNodes, edges: copiedEdges, } setClipboardData(clipboardData) void writeWorkflowClipboard(clipboardData, appDslVersion).catch(() => {}) }, [getNodesReadOnly, workflowStore, collaborativeWorkflow, isNodeCopyable, appDslVersion], ) const handleNodesPaste = useCallback(async () => { if (getNodesReadOnly()) return const { clipboardElements: storeClipboardElements, clipboardEdges: storeClipboardEdges, mousePosition, setClipboardData, } = workflowStore.getState() const clipboardData = await readWorkflowClipboard(appDslVersion) const hasSystemClipboard = clipboardData.nodes.length > 0 const shouldRunCompatibilityCheck = hasSystemClipboard && clipboardData.isVersionMismatch const clipboardElements = hasSystemClipboard ? clipboardData.nodes : storeClipboardElements const clipboardEdges = hasSystemClipboard ? clipboardData.edges : storeClipboardEdges if (hasSystemClipboard) setClipboardData(clipboardData) const validatedClipboardElements = clipboardElements.filter(isClipboardNodeStructurallyValid) const validatedClipboardEdges = clipboardEdges.filter(isClipboardEdgeStructurallyValid) if (!validatedClipboardElements.length) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const reservedNodeTitles = new Set( nodes .map((node) => node.data.title) .filter((title): title is string => typeof title === 'string'), ) const nodesToPaste: Node[] = [] const edgesToPaste: Edge[] = [] let compatibleClipboardElements = validatedClipboardElements.filter((node) => { if (node.type === CUSTOM_NOTE_NODE) return true const nodeDefaultValue = getNodeDefaultValueForPaste(node) if (!nodeDefaultValue) return false if ( shouldRunCompatibilityCheck && !isClipboardValueCompatibleWithDefault(nodeDefaultValue, node.data) ) { return false } return true }) if (shouldRunCompatibilityCheck) { compatibleClipboardElements = pruneClipboardNodesWithFilteredAncestors( validatedClipboardElements, compatibleClipboardElements, ) } const compatibleClipboardNodeIds = new Set(compatibleClipboardElements.map((node) => node.id)) const filteredNodeCount = shouldRunCompatibilityCheck ? validatedClipboardElements.length - compatibleClipboardElements.length : 0 const filteredEdgeCount = shouldRunCompatibilityCheck ? validatedClipboardEdges.filter( (edge) => !compatibleClipboardNodeIds.has(edge.source) || !compatibleClipboardNodeIds.has(edge.target), ).length : 0 if (shouldRunCompatibilityCheck && (filteredNodeCount > 0 || filteredEdgeCount > 0)) { toast.warning( t(($) => $['common.clipboardVersionCompatibilityWarning'], { ns: 'workflow', }), ) } if (!compatibleClipboardElements.length) return const rootClipboardNodes = compatibleClipboardElements.filter( (node) => !node.parentId || !compatibleClipboardNodeIds.has(node.parentId), ) const positionReferenceNodes = rootClipboardNodes.length ? rootClipboardNodes : compatibleClipboardElements const { x, y } = getTopLeftNodePosition(positionReferenceNodes) const { screenToFlowPosition } = reactflow const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY, }) const offsetX = currentPosition.x - x const offsetY = currentPosition.y - y let idMapping: Record = {} const pastedNodesMap: Record = {} const parentChildrenToAppend: { parentId: string; childId: string; childType: BlockEnum }[] = [] const selectedNodes = nodes.filter((node) => node.selected) // Keep this list aligned with availableBlocksFilter(inContainer) // in use-available-blocks.ts. const commonNestedDisallowPasteNodes = [ BlockEnum.End, BlockEnum.Iteration, BlockEnum.Loop, BlockEnum.DataSource, BlockEnum.KnowledgeBase, BlockEnum.HumanInput, ] // Same-canvas copy keeps the source container selected, so only treat a // selected container as the paste target when it is not part of the clipboard. const selectedContainerNode = selectedNodes.length === 1 && (selectedNodes[0]?.data.type === BlockEnum.Iteration || selectedNodes[0]?.data.type === BlockEnum.Loop) && !compatibleClipboardNodeIds.has(selectedNodes[0].id) ? selectedNodes[0] : undefined rootClipboardNodes.forEach((nodeToPaste, index) => { const nodeDefaultValue = getNodeDefaultValueForPaste(nodeToPaste) if (nodeToPaste.type !== CUSTOM_NOTE_NODE && !nodeDefaultValue) return if (selectedContainerNode && commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type)) return const mergedData = shouldRunCompatibilityCheck ? (sanitizeClipboardValueByDefault(nodeDefaultValue ?? {}, nodeToPaste.data) as Record< string, unknown >) : { ...(nodeToPaste.type !== CUSTOM_NOTE_NODE ? nodeDefaultValue : {}), ...nodeToPaste.data, } const sourceTitle = typeof mergedData.title === 'string' ? mergedData.title : typeof nodeToPaste.data.title === 'string' ? nodeToPaste.data.title : 'Node' const sourceDesc = typeof mergedData.desc === 'string' ? mergedData.desc : typeof nodeToPaste.data.desc === 'string' ? nodeToPaste.data.desc : '' const { newNode, newIterationStartNode, newLoopStartNode } = generateNewNode({ type: nodeToPaste.type, data: { ...mergedData, type: nodeToPaste.data.type, desc: sourceDesc, selected: false, _isBundled: false, _connectedSourceHandleIds: [], _connectedTargetHandleIds: [], _dimmed: false, isInIteration: false, iteration_id: undefined, isInLoop: false, loop_id: undefined, title: getUniquePastedNodeTitle(sourceTitle, reservedNodeTitles), }, position: { x: nodeToPaste.position.x + offsetX, y: nodeToPaste.position.y + offsetY, }, extent: nodeToPaste.extent, zIndex: 0, }) newNode.id = newNode.id + index let newChildren: Node[] = [] if (nodeToPaste.data.type === BlockEnum.Iteration) { if (newIterationStartNode) { newIterationStartNode.parentId = newNode.id const iterationNodeData = newNode.data as IterationNodeType iterationNodeData.start_node_id = newIterationStartNode.id } const oldIterationStartNodeInClipboard = compatibleClipboardElements.find( (n) => n.parentId === nodeToPaste.id && n.type === CUSTOM_ITERATION_START_NODE, ) if (oldIterationStartNodeInClipboard && newIterationStartNode) idMapping[oldIterationStartNodeInClipboard.id] = newIterationStartNode.id const copiedIterationChildren = compatibleClipboardElements.filter( (n) => n.parentId === nodeToPaste.id && n.type !== CUSTOM_ITERATION_START_NODE, ) if (copiedIterationChildren.length) { copiedIterationChildren.forEach((child, childIndex) => { const childType = child.data.type const childDefaultValue = getNodeDefaultValueForPaste(child) if (child.type !== CUSTOM_NOTE_NODE && !childDefaultValue) return const mergedChildData = shouldRunCompatibilityCheck ? (sanitizeClipboardValueByDefault(childDefaultValue ?? {}, child.data) as Record< string, unknown >) : { ...(child.type !== CUSTOM_NOTE_NODE ? childDefaultValue : {}), ...child.data, } const childSourceTitle = typeof mergedChildData.title === 'string' ? mergedChildData.title : typeof child.data.title === 'string' ? child.data.title : 'Node' const childSourceDesc = typeof mergedChildData.desc === 'string' ? mergedChildData.desc : typeof child.data.desc === 'string' ? child.data.desc : '' const { newNode: newChild } = generateNewNode({ type: child.type, data: { ...mergedChildData, desc: childSourceDesc, selected: false, _isBundled: false, _connectedSourceHandleIds: [], _connectedTargetHandleIds: [], _dimmed: false, title: getUniquePastedNodeTitle(childSourceTitle, reservedNodeTitles), isInIteration: true, iteration_id: newNode.id, isInLoop: false, loop_id: undefined, type: childType, }, position: child.position, positionAbsolute: child.positionAbsolute, parentId: newNode.id, extent: child.extent, zIndex: NESTED_ELEMENT_Z_INDEX, }) newChild.id = `${newNode.id}${newChild.id + childIndex}` idMapping[child.id] = newChild.id newChildren.push(newChild) }) } else { const oldIterationStartNode = nodes.find( (n) => n.parentId === nodeToPaste.id && n.type === CUSTOM_ITERATION_START_NODE, ) if (oldIterationStartNode && newIterationStartNode) idMapping[oldIterationStartNode.id] = newIterationStartNode.id const { copyChildren, newIdMapping } = handleNodeIterationChildrenCopy( nodeToPaste.id, newNode.id, idMapping, ) newChildren = copyChildren idMapping = newIdMapping } newChildren.forEach((child) => { newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type, }) }) if (newIterationStartNode) newChildren.push(newIterationStartNode) } else if (nodeToPaste.data.type === BlockEnum.Loop) { if (newLoopStartNode) { newLoopStartNode.parentId = newNode.id const loopNodeData = newNode.data as LoopNodeType loopNodeData.start_node_id = newLoopStartNode.id } const oldLoopStartNodeInClipboard = compatibleClipboardElements.find( (n) => n.parentId === nodeToPaste.id && n.type === CUSTOM_LOOP_START_NODE, ) if (oldLoopStartNodeInClipboard && newLoopStartNode) idMapping[oldLoopStartNodeInClipboard.id] = newLoopStartNode.id const copiedLoopChildren = compatibleClipboardElements.filter( (n) => n.parentId === nodeToPaste.id && n.type !== CUSTOM_LOOP_START_NODE, ) if (copiedLoopChildren.length) { copiedLoopChildren.forEach((child, childIndex) => { const childType = child.data.type const childDefaultValue = getNodeDefaultValueForPaste(child) if (child.type !== CUSTOM_NOTE_NODE && !childDefaultValue) return const mergedChildData = shouldRunCompatibilityCheck ? (sanitizeClipboardValueByDefault(childDefaultValue ?? {}, child.data) as Record< string, unknown >) : { ...(child.type !== CUSTOM_NOTE_NODE ? childDefaultValue : {}), ...child.data, } const childSourceTitle = typeof mergedChildData.title === 'string' ? mergedChildData.title : typeof child.data.title === 'string' ? child.data.title : 'Node' const childSourceDesc = typeof mergedChildData.desc === 'string' ? mergedChildData.desc : typeof child.data.desc === 'string' ? child.data.desc : '' const { newNode: newChild } = generateNewNode({ type: child.type, data: { ...mergedChildData, desc: childSourceDesc, selected: false, _isBundled: false, _connectedSourceHandleIds: [], _connectedTargetHandleIds: [], _dimmed: false, title: getUniquePastedNodeTitle(childSourceTitle, reservedNodeTitles), isInIteration: false, iteration_id: undefined, isInLoop: true, loop_id: newNode.id, type: childType, }, position: child.position, positionAbsolute: child.positionAbsolute, parentId: newNode.id, extent: child.extent, zIndex: NESTED_ELEMENT_Z_INDEX, }) newChild.id = `${newNode.id}${newChild.id + childIndex}` idMapping[child.id] = newChild.id newChildren.push(newChild) }) } else { const oldLoopStartNode = nodes.find( (n) => n.parentId === nodeToPaste.id && n.type === CUSTOM_LOOP_START_NODE, ) if (oldLoopStartNode && newLoopStartNode) idMapping[oldLoopStartNode.id] = newLoopStartNode.id const { copyChildren, newIdMapping } = handleNodeLoopChildrenCopy( nodeToPaste.id, newNode.id, idMapping, ) newChildren = copyChildren idMapping = newIdMapping } newChildren.forEach((child) => { newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type, }) }) if (newLoopStartNode) newChildren.push(newLoopStartNode) } else if (selectedContainerNode) { if ( selectedContainerNode.data.type === BlockEnum.Iteration || selectedContainerNode.data.type === BlockEnum.Loop ) { const isIteration = selectedContainerNode.data.type === BlockEnum.Iteration newNode.data.isInIteration = isIteration newNode.data.iteration_id = isIteration ? selectedContainerNode.id : undefined newNode.data.isInLoop = !isIteration newNode.data.loop_id = !isIteration ? selectedContainerNode.id : undefined newNode.parentId = selectedContainerNode.id newNode.zIndex = NESTED_ELEMENT_Z_INDEX newNode.positionAbsolute = { x: newNode.position.x, y: newNode.position.y, } newNode.position = getNestedNodePosition(newNode, selectedContainerNode) parentChildrenToAppend.push({ parentId: selectedContainerNode.id, childId: newNode.id, childType: newNode.data.type, }) } } idMapping[nodeToPaste.id] = newNode.id nodesToPaste.push(newNode) pastedNodesMap[newNode.id] = newNode if (newChildren.length) { newChildren.forEach((child) => { pastedNodesMap[child.id] = child }) nodesToPaste.push(...newChildren) } }) const sourceEdges = validatedClipboardEdges sourceEdges.forEach((edge) => { const sourceId = idMapping[edge.source] const targetId = idMapping[edge.target] if (sourceId && targetId) { const sourceNode = pastedNodesMap[sourceId] const targetNode = pastedNodesMap[targetId] if (!sourceNode || !targetNode) return const parentNode = sourceNode.parentId && sourceNode.parentId === targetNode.parentId ? (pastedNodesMap[sourceNode.parentId] ?? nodes.find((n) => n.id === sourceNode.parentId)) : null const isInIteration = parentNode?.data.type === BlockEnum.Iteration const isInLoop = parentNode?.data.type === BlockEnum.Loop const newEdge: Edge = { ...edge, id: `${sourceId}-${edge.sourceHandle}-${targetId}-${edge.targetHandle}`, source: sourceId, target: targetId, data: { ...(edge.data || {}), sourceType: sourceNode.data.type, targetType: targetNode.data.type, isInIteration, iteration_id: isInIteration ? parentNode?.id : undefined, isInLoop, loop_id: isInLoop ? parentNode?.id : undefined, _connectedNodeIsSelected: false, }, zIndex: parentNode && (isInIteration || isInLoop) ? NESTED_ELEMENT_Z_INDEX : 0, } edgesToPaste.push(newEdge) } }) const newNodes = produce(nodes, (draft: Node[]) => { parentChildrenToAppend.forEach(({ parentId, childId, childType }) => { const p = draft.find((n) => n.id === parentId) if (p) p.data._children?.push({ nodeId: childId, nodeType: childType }) }) draft.push(...nodesToPaste) }) setNodes(newNodes) setEdges([...edges, ...edgesToPaste]) saveStateToHistory(WorkflowHistoryEvent.NodePaste, { nodeId: nodesToPaste?.[0]?.id, }) handleSyncWorkflowDraft() }, [ getNodesReadOnly, workflowStore, collaborativeWorkflow, reactflow, t, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy, getNodeDefaultValueForPaste, appDslVersion, ]) const handleNodesDuplicate = useCallback( (nodeId?: string) => { if (getNodesReadOnly()) return handleNodesCopy(nodeId) handleNodesPaste() }, [getNodesReadOnly, handleNodesCopy, handleNodesPaste], ) const handleNodesDelete = useCallback(() => { if (getNodesReadOnly()) return const { nodes, edges } = collaborativeWorkflow.getState() const bundledNodes = nodes.filter((node) => node.data._isBundled) if (bundledNodes.length) { bundledNodes.forEach((node) => handleNodeDelete(node.id)) return } const edgeSelected = edges.some((edge) => edge.selected) if (edgeSelected) return const selectedNode = nodes.find((node) => node.data.selected) if (selectedNode) handleNodeDelete(selectedNode.id) }, [collaborativeWorkflow, getNodesReadOnly, handleNodeDelete]) const handleNodeResize = useCallback( (nodeId: string, params: ResizeParamsWithDirection) => { if (getNodesReadOnly()) return const { nodes, setNodes } = collaborativeWorkflow.getState() const { x, y, width, height } = params const currentNode = nodes.find((n) => n.id === nodeId)! const childrenNodes = nodes.filter((n) => currentNode.data._children?.find( (child: NonNullable[number]) => child.nodeId === n.id, ), ) let rightNode: Node let bottomNode: Node childrenNodes.forEach((n) => { if (rightNode) { if (n.position.x + n.width! > rightNode.position.x + rightNode.width!) rightNode = n } else { rightNode = n } if (bottomNode) { if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!) { bottomNode = n } } else { bottomNode = n } }) if (rightNode! && bottomNode!) { const parentNode = nodes.find((n) => n.id === rightNode.parentId) const paddingMap = parentNode?.data.type === BlockEnum.Iteration ? ITERATION_PADDING : LOOP_PADDING if (width < rightNode!.position.x + rightNode.width! + paddingMap.right) return if (height < bottomNode.position.y + bottomNode.height! + paddingMap.bottom) { return } } const newNodes = produce(nodes, (draft) => { draft.forEach((n) => { if (n.id === nodeId) { n.data.width = width n.data.height = height n.width = width n.height = height n.position.x = x n.position.y = y } }) }) setNodes(newNodes) handleSyncWorkflowDraft() saveStateToHistory(WorkflowHistoryEvent.NodeResize, { nodeId }) }, [getNodesReadOnly, collaborativeWorkflow, handleSyncWorkflowDraft, saveStateToHistory], ) const handleNodeDisconnect = useCallback( (nodeId: string) => { if (getNodesReadOnly()) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const currentNode = nodes.find((node) => node.id === nodeId)! const connectedEdges = getConnectedEdges([currentNode], edges) const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( connectedEdges.map((edge) => ({ type: 'remove', edge })), nodes, ) const newNodes = produce(nodes, (draft: Node[]) => { draft.forEach((node) => { if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { node.data = { ...node.data, ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], } } }) }) setNodes(newNodes) const newEdges = produce(edges, (draft) => { return draft.filter( (edge) => !connectedEdges.some((connectedEdge) => connectedEdge.id === edge.id), ) }) setEdges(newEdges) handleSyncWorkflowDraft() saveStateToHistory(WorkflowHistoryEvent.EdgeDelete) }, [collaborativeWorkflow, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory], ) const handleHistoryBack = useCallback(() => { if (getNodesReadOnly() || getWorkflowReadOnly()) return undo() const { edges, nodes } = workflowHistoryStore.getState() if (edges.length === 0 && nodes.length === 0) return const { setNodes, setEdges } = collaborativeWorkflow.getState() const shouldBroadcast = collaborationManager.isConnected() setEdges(edges, shouldBroadcast) setNodes(nodes, shouldBroadcast, 'nodes:history-back') if (shouldBroadcast) collaborationManager.emitHistoryAction('undo') workflowStore.setState({ contextMenuTarget: undefined }) }, [ collaborativeWorkflow, workflowStore, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly, ]) const handleHistoryForward = useCallback(() => { if (getNodesReadOnly() || getWorkflowReadOnly()) return redo() const { edges, nodes } = workflowHistoryStore.getState() if (edges.length === 0 && nodes.length === 0) return const { setNodes, setEdges } = collaborativeWorkflow.getState() const shouldBroadcast = collaborationManager.isConnected() setEdges(edges, shouldBroadcast) setNodes(nodes, shouldBroadcast, 'nodes:history-forward') if (shouldBroadcast) collaborationManager.emitHistoryAction('redo') workflowStore.setState({ contextMenuTarget: undefined }) }, [ collaborativeWorkflow, redo, workflowStore, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly, ]) const [isDimming, setIsDimming] = useState(false) /** Add opacity-30 to all nodes except the nodeId */ const dimOtherNodes = useCallback(() => { if (isDimming) return const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() const selectedNode = nodes.find((n) => n.data.selected) if (!selectedNode) return setIsDimming(true) // const workflowNodes = useStore(s => s.getNodes()) const workflowNodes = nodes const usedVars = getNodeUsedVars(selectedNode) const dependencyNodes: Node[] = [] usedVars.forEach((valueSelector) => { const node = workflowNodes.find((node) => node.id === valueSelector?.[0]) if (node) { if (!dependencyNodes.includes(node)) dependencyNodes.push(node) } }) const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) for (let currIdx = 0; currIdx < outgoers.length; currIdx++) { const node = outgoers[currIdx] const outgoersForNode = getOutgoers(node!, nodes as Node[], edges) outgoersForNode.forEach((item) => { const existed = outgoers.some((v) => v.id === item.id) if (!existed) outgoers.push(item) }) } const dependentNodes: Node[] = [] outgoers.forEach((node) => { const usedVars = getNodeUsedVars(node) const used = usedVars.some((v) => v?.[0] === selectedNode.id) if (used) { const existed = dependentNodes.some((v) => v.id === node.id) if (!existed) dependentNodes.push(node) } }) const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode] const newNodes = produce(nodes, (draft) => { draft.forEach((n) => { const dimNode = dimNodes.find((v) => v.id === n.id) if (!dimNode) n.data._dimmed = true }) }) setNodes(newNodes) const tempEdges: Edge[] = [] dependencyNodes.forEach((n) => { tempEdges.push({ id: `tmp_${n.id}-source-${selectedNode.id}-target`, type: CUSTOM_EDGE, source: n.id, sourceHandle: 'source_tmp', target: selectedNode.id, targetHandle: 'target_tmp', animated: true, data: { sourceType: n.data.type, targetType: selectedNode.data.type, _isTemp: true, _connectedNodeIsHovering: true, }, }) }) dependentNodes.forEach((n) => { tempEdges.push({ id: `tmp_${selectedNode.id}-source-${n.id}-target`, type: CUSTOM_EDGE, source: selectedNode.id, sourceHandle: 'source_tmp', target: n.id, targetHandle: 'target_tmp', animated: true, data: { sourceType: selectedNode.data.type, targetType: n.data.type, _isTemp: true, _connectedNodeIsHovering: true, }, }) }) const newEdges = produce(edges, (draft) => { draft.forEach((e) => { e.data._dimmed = true }) draft.push(...tempEdges) }) setEdges(newEdges) }, [isDimming, collaborativeWorkflow]) /** Restore all nodes to full opacity */ const undimAllNodes = useCallback(() => { const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState() setIsDimming(false) const newNodes = produce(nodes, (draft) => { draft.forEach((n) => { n.data._dimmed = false }) }) setNodes(newNodes) const newEdges = produce( edges.filter((e) => !e.data._isTemp), (draft) => { draft.forEach((e) => { e.data._dimmed = false }) }, ) setEdges(newEdges) }, [collaborativeWorkflow]) return { handleNodeDragStart, handleNodeDrag, handleNodeDragStop, handleNodeEnter, handleNodeLeave, handleNodeSelect, handleNodeClick, handleNodeConnect, handleNodeConnectStart, handleNodeConnectEnd, handleNodeDelete, handleNodeChange, handleNodeAdd, handleNodesCancelSelected, handleNodeContextMenu, handleNodesCopy, handleNodesPaste, handleNodesDuplicate, handleNodesDelete, handleNodeResize, handleNodeDisconnect, handleHistoryBack, handleHistoryForward, dimOtherNodes, undimAllNodes, } }