diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 494d429a82..40b855b81a 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -255,7 +255,7 @@ export const RETRIEVAL_OUTPUT_STRUCT = `{ export const SUPPORT_OUTPUT_VARS_NODE = [ BlockEnum.Start, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform, - BlockEnum.QuestionClassifier, BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, + BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, ] const USAGE = { diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index b60d9c629f..d9bfff1110 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1,9 +1,11 @@ import { useCallback, useRef } from 'react' import produce from 'immer' import type { + HandleType, NodeDragHandler, NodeMouseHandler, OnConnect, + OnConnectStart, } from 'reactflow' import { getConnectedEdges, @@ -25,16 +27,21 @@ import { generateNewNode, getNodesConnectedSourceOrTargetHandleIdsMap, } from '../utils' -import { useNodesInitialData } from './use-nodes-data' +import { + useNodesExtraData, + useNodesInitialData, +} from './use-nodes-data' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useWorkflow } from './use-workflow' export const useNodesInteractions = () => { const store = useStoreApi() const nodesInitialData = useNodesInitialData() + const nodesExtraData = useNodesExtraData() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { getAfterNodesInSameBranch } = useWorkflow() const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number }) + const connectingNodeRef = useRef<{ nodeId: string; handleType: HandleType } | null>(null) const handleNodeDragStart = useCallback((_, node) => { const { @@ -177,9 +184,27 @@ export const useNodesInteractions = () => { return const { + getNodes, + setNodes, edges, setEdges, } = store.getState() + const nodes = getNodes() + + if (connectingNodeRef.current && connectingNodeRef.current.nodeId !== node.id) { + const connectingNode: Node = nodes.find(n => n.id === connectingNodeRef.current!.nodeId)! + const handleType = connectingNodeRef.current.handleType + const currentNodeIndex = nodes.findIndex(n => n.id === node.id) + const availablePrevNodes = nodesExtraData[connectingNode.data.type].availablePrevNodes + const availableNextNodes = nodesExtraData[connectingNode.data.type].availableNextNodes + const availableNodes = handleType === 'source' ? availableNextNodes : [...availablePrevNodes, BlockEnum.Start] + + const newNodes = produce(nodes, (draft) => { + if (!availableNodes.includes(draft[currentNodeIndex].data.type)) + draft[currentNodeIndex].data._isInvalidConnection = true + }) + setNodes(newNodes) + } const newEdges = produce(edges, (draft) => { const connectedEdges = getConnectedEdges([node], edges) @@ -190,7 +215,7 @@ export const useNodesInteractions = () => { }) }) setEdges(newEdges) - }, [store]) + }, [store, nodesExtraData]) const handleNodeLeave = useCallback(() => { const { runningStatus } = useStore.getState() @@ -199,9 +224,17 @@ export const useNodesInteractions = () => { return const { + getNodes, + setNodes, edges, setEdges, } = store.getState() + const newNodes = produce(getNodes(), (draft) => { + draft.forEach((node) => { + node.data._isInvalidConnection = false + }) + }) + setNodes(newNodes) const newEdges = produce(edges, (draft) => { draft.forEach((edge) => { edge.data = { ...edge.data, _connectedNodeIsHovering: false } @@ -307,6 +340,19 @@ export const useNodesInteractions = () => { handleSyncWorkflowDraft() }, [store, handleSyncWorkflowDraft]) + const handleNodeConnectStart = useCallback((_, { nodeId, handleType }) => { + if (nodeId && handleType) { + connectingNodeRef.current = { + nodeId, + handleType, + } + } + }, []) + + const handleNodeConnectEnd = useCallback(() => { + connectingNodeRef.current = null + }, []) + const handleNodeDelete = useCallback((nodeId: string) => { const { runningStatus } = useStore.getState() @@ -596,6 +642,8 @@ export const useNodesInteractions = () => { handleNodeSelect, handleNodeClick, handleNodeConnect, + handleNodeConnectStart, + handleNodeConnectEnd, handleNodeDelete, handleNodeChange, handleNodeAdd, diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index ee2a5bb303..4f1e855d30 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -9,6 +9,7 @@ import { getOutgoers, useStoreApi, } from 'reactflow' +import type { Connection } from 'reactflow' import type { ToolsMap } from '../block-selector/types' import { generateNewNode, @@ -21,7 +22,10 @@ import { START_INITIAL_POSITION, SUPPORT_OUTPUT_VARS_NODE, } from '../constants' -import { useNodesInitialData } from './use-nodes-data' +import { + useNodesExtraData, + useNodesInitialData, +} from './use-nodes-data' import { useStore as useAppStore } from '@/app/components/app/store' import { fetchNodesDefaultConfigs, @@ -38,6 +42,7 @@ export const useIsChatMode = () => { export const useWorkflow = () => { const store = useStoreApi() + const nodesExtraData = useNodesExtraData() const handleLayout = useCallback(async () => { const { @@ -169,9 +174,24 @@ export const useWorkflow = () => { return list }, [store]) - const isValidConnection = useCallback(() => { + const isValidConnection = useCallback(({ source, target }: Connection) => { + const { getNodes } = store.getState() + const nodes = getNodes() + const sourceNode: Node = nodes.find(node => node.id === source)! + const targetNode: Node = nodes.find(node => node.id === target)! + + if (sourceNode && targetNode) { + const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes + const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start] + if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) + return false + + if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type)) + return false + } + return true - }, []) + }, [store, nodesExtraData]) return { handleLayout, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 54b572b169..6c827fd407 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -10,7 +10,6 @@ import ReactFlow, { Background, ReactFlowProvider, useOnViewportChange, - useStoreApi, } from 'reactflow' import type { Viewport } from 'reactflow' import 'reactflow/dist/style.css' @@ -62,7 +61,6 @@ const Workflow: FC = memo(({ const showFeaturesPanel = useStore(state => state.showFeaturesPanel) const runningStatus = useStore(s => s.runningStatus) const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const store = useStoreApi() useEffect(() => { setAutoFreeze(false) @@ -80,6 +78,8 @@ const Workflow: FC = memo(({ handleNodeLeave, handleNodeClick, handleNodeConnect, + handleNodeConnectStart, + handleNodeConnectEnd, } = useNodesInteractions() const { handleEdgeEnter, @@ -108,6 +108,7 @@ const Workflow: FC = memo(({ } = memo(({ onNodeMouseLeave={handleNodeLeave} onNodeClick={handleNodeClick} onConnect={handleNodeConnect} + onConnectStart={handleNodeConnectStart} + onConnectEnd={handleNodeConnectEnd} onEdgeMouseEnter={handleEdgeEnter} onEdgeMouseLeave={handleEdgeLeave} onEdgesChange={handleEdgesChange} diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 84f99c56bc..978f944441 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -77,7 +77,7 @@ export const NodeTargetHandle = memo(({ onClick={handleHandleClick} > { - !connected && isConnectable && ( + !connected && isConnectable && !data._isInvalidConnection && ( { - !connected && isConnectable && ( + !connected && isConnectable && !data._isInvalidConnection && ( = ({
= ({ ${data._runningStatus === NodeRunningStatus.Succeeded && '!border-[#12B76A]'} ${data._runningStatus === NodeRunningStatus.Failed && '!border-[#F04438]'} ${data._runningStatus === NodeRunningStatus.Waiting && 'opacity-70'} + ${data._isInvalidConnection && '!border-[#F04438]'} `} > { diff --git a/web/app/components/workflow/style.css b/web/app/components/workflow/style.css deleted file mode 100644 index 29d13acb6b..0000000000 --- a/web/app/components/workflow/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.react-flow__node { - transition: transform 0.1s ease-in-out; -} \ No newline at end of file diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 80f3209765..22cf19f1bf 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -26,6 +26,7 @@ export type Branch = { } export type CommonNodeType = { + _isInvalidConnection?: boolean _connectedSourceHandleIds?: string[] _connectedTargetHandleIds?: string[] _targetBranches?: Branch[]