// File: frontend/app/workflow/page.tsx (更新) // Description: 工作流编辑器主页面,引入模块化组件 'use client'; import React, { useState, useCallback, useRef, DragEvent, useMemo } from 'react'; import ReactFlow, { ReactFlowProvider, MiniMap, Controls, Background, useNodesState, useEdgesState, addEdge, Node, Edge, Connection, Position, MarkerType, NodeChange, EdgeChange, applyNodeChanges, applyEdgeChanges, useReactFlow, XYPosition, BackgroundVariant, Panel, } from 'reactflow'; // 引入自定义节点和侧边栏组件 import { StartNode } from './components/StartNode'; import { OutputNode } from './components/OutputNode'; import { LLMNode } from './components/LLMNode'; import { WorkflowSidebar } from './components/WorkflowSidebar'; import { ChatInputNode } from './components/ChatInputNode'; // 引入新节点 import { ChatOutputNode } from './components/ChatOutputNode'; // 引入新节点 // 引入类型 (如果需要) // import type { LLMNodeData } from './components/types'; // 引入 API 函数 import { runWorkflow } from '@/lib/api'; // Import runWorkflow import 'reactflow/dist/style.css'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { LoaderIcon, PlayIcon } from 'lucide-react'; // --- 初始节点和边数据 --- const initialNodes: Node[] = [ ]; const initialEdges: Edge[] = []; // --- 工作流编辑器组件 --- function WorkflowEditor() { const reactFlowWrapper = useRef(null); // Ref 指向 React Flow 容器 const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const { screenToFlowPosition } = useReactFlow(); const [isRunning, setIsRunning] = useState(false); // State for run button loading // --- 注册自定义节点类型 --- const nodeTypes = useMemo(() => ({ startNode: StartNode, outputNode: OutputNode, llmNode: LLMNode, // LLMNode 现在需要一种方式来调用 updateNodeData chatInputNode: ChatInputNode, // 注册 ChatInputNode chatOutputNode: ChatOutputNode, // 注册 ChatOutputNode // inputNode: CustomInputNode, // ragNode: RAGNode, }), []); // 移除 updateNodeData 依赖,因为它不应该直接传递 const onConnect = useCallback( (connection: Connection) => { setEdges((eds) => addEdge({ ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, eds)); if (connection.targetHandle === 'input-text' && connection.target) { setNodes((nds) => nds.map((node) => { if (node.id === connection.target) { return { ...node, data: { ...node.data, inputConnected: true } }; } return node; }) ); } // 更新 ChatOutputNode 的连接状态 (如果需要) if (connection.targetHandle === 'message-input' && connection.target) { setNodes((nds) => nds.map((node) => node.id === connection.target ? { ...node, data: { ...node.data, inputConnected: true } } : node)); } }, [setEdges, setNodes] // 保持 updateNodeData 依赖 ); const onEdgesChangeIntercepted = useCallback( (changes: EdgeChange[]) => { changes.forEach(change => { if (change.type === 'remove') { const edgeToRemove = edges.find(edge => edge.id === change.id); if (edgeToRemove?.targetHandle === 'input-text' && edgeToRemove.target) { setNodes((nds) => nds.map((node) => { if (node.id === edgeToRemove.target) { return { ...node, data: { ...node.data, inputConnected: false } }; } return node; }) ); } // 更新 ChatOutputNode 的断开状态 (如果需要) if (edgeToRemove?.targetHandle === 'message-input' && edgeToRemove.target) { setNodes((nds) => nds.map((node) => node.id === edgeToRemove.target ? { ...node, data: { ...node.data, inputConnected: false } } : node)); } } }); onEdgesChange(changes); }, [edges, onEdgesChange, setNodes] // 保持 updateNodeData 依赖 ); // 处理画布上的拖拽悬停事件 const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); // 必须阻止默认行为才能触发 onDrop event.dataTransfer.dropEffect = 'move'; // 设置放置效果 }, []); // 处理画布上的放置事件 const onDrop = useCallback( (event: DragEvent) => { event.preventDefault(); // 阻止默认行为(如打开文件) if (!reactFlowWrapper.current) { return; } // 从 dataTransfer 中获取节点信息 const nodeInfoString = event.dataTransfer.getData('application/reactflow'); if (!nodeInfoString) { console.warn("No reactflow data found in dataTransfer"); return; } let nodeInfo; try { nodeInfo = JSON.parse(nodeInfoString); } catch (error) { console.error("Failed to parse node info from dataTransfer", error); return; } const { nodeType, defaultData } = nodeInfo; if (!nodeType) { console.warn("Node type not found in dataTransfer"); return; } // 获取鼠标相对于 React Flow 画布的位置 // 需要计算鼠标位置相对于 reactFlowWrapper 的偏移量 const position = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); // 创建新节点 const newNode: Node = { id: `${nodeType}-${Date.now()}`, // 使用更唯一的 ID type: nodeType, // 设置节点类型 position, data: { label: defaultData?.label || `${nodeType} node`, ...defaultData }, // 根据节点类型设置 Handle 位置 (可选,也可以在自定义节点内部定义) // sourcePosition: Position.Bottom, // targetPosition: Position.Top, }; console.log('Node dropped:', newNode); // 将新节点添加到状态中 setNodes((nds) => nds.concat(newNode)); }, [screenToFlowPosition, setNodes] // 依赖 project 方法和 setNodes ); // --- 处理工作流运行 --- const handleRunWorkflow = useCallback(async () => { setIsRunning(true); toast.info("正在执行工作流..."); console.log("Running workflow with nodes:", nodes); console.log("Running workflow with edges:", edges); try { const result = await runWorkflow(nodes, edges); // Call the API function if (result.success && result.output !== undefined && result.output_node_id) { toast.success(result.message || "工作流执行成功!"); console.log("Workflow output:", result.output); // 更新 ChatOutputNode 的数据以显示结果 setNodes((nds) => nds.map((node) => { if (node.id === result.output_node_id && node.type === 'chatOutputNode') { return { ...node, data: { ...node.data, displayText: result.output } }; } return node; }) ); } else { toast.error(result.message || "工作流执行失败。"); console.error("Workflow execution failed:", result.message); } } catch (error) { // This catch block might not be necessary if runWorkflow handles errors toast.error("执行工作流时发生网络错误。"); console.error("API call error:", error); } finally { setIsRunning(false); } }, [nodes, edges, setNodes]); // Depend on nodes and edges return (

工作流编辑器

{/* 主容器使用 Flexbox */}
{/* 侧边栏 */} {/* React Flow 画布容器 */}
{/* 添加 ref */} {/* 使用 Panel 添加运行按钮到右上角 */}
); } // --- 主页面组件,包含 Provider --- export default function WorkflowPage() { return ( ); }