diff --git a/frontend/app/workflow/page.tsx b/frontend/app/workflow/page.tsx index 286afe0..8f13a24 100644 --- a/frontend/app/workflow/page.tsx +++ b/frontend/app/workflow/page.tsx @@ -1,129 +1,171 @@ // File: frontend/app/workflow/page.tsx -// Description: 工作流编辑器页面,使用 React Flow +// Description: 工作流编辑器页面,添加侧边栏用于拖放节点 -'use client'; // React Flow 需要客户端渲染 +'use client'; -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback, useRef, DragEvent } from 'react'; // 添加 useRef, DragEvent import ReactFlow, { - ReactFlowProvider, // Provider 包裹应用 - MiniMap, // 小地图 - Controls, // 控制按钮 (缩放, 适应视图) - Background, // 背景网格 - useNodesState, // Hook 管理节点状态 - useEdgesState, // Hook 管理边状态 - addEdge, // Helper 函数添加边 - Node, // 节点类型 - Edge, // 边类型 - Connection, // 连接类型 - Position, // 用于 Handle 定位 - MarkerType, // 用于边箭头类型 - NodeChange, // 节点变化类型 - EdgeChange, // 边变化类型 - applyNodeChanges, // 应用节点变化 + ReactFlowProvider, + MiniMap, + Controls, + Background, + useNodesState, + useEdgesState, + addEdge, + Node, + Edge, + Connection, + Position, + MarkerType, + NodeChange, + EdgeChange, + applyNodeChanges, applyEdgeChanges, - BackgroundVariant, // 应用边变化 + useReactFlow, // 导入 useReactFlow hook + XYPosition, + BackgroundVariant, // 导入 XYPosition 类型 } from 'reactflow'; +import { MessageSquareText, BrainCircuit, Database, LogOut, Play } from 'lucide-react'; // 导入一些节点图标 -// 引入 React Flow 的 CSS 样式 import 'reactflow/dist/style.css'; -// --- 初始节点和边数据 (示例) --- +// --- 初始节点和边数据 (可以清空或保留示例) --- const initialNodes: Node[] = [ - { - id: '1', - type: 'input', // React Flow 内建输入类型节点 - data: { label: '开始节点' }, - position: { x: 250, y: 5 }, - sourcePosition: Position.Bottom, // Handle (连接点) 位置 - }, - { - id: '2', - // type: 'default', // 默认类型节点 - data: { label: '处理节点 A' }, - position: { x: 100, y: 100 }, - sourcePosition: Position.Bottom, - targetPosition: Position.Top, - }, - { - id: '3', - type: 'output', // React Flow 内建输出类型节点 - data: { label: '结束节点' }, - position: { x: 400, y: 100 }, - targetPosition: Position.Top, - }, - { - id: '4', - type: 'default', // 自定义节点类型后续添加 - data: { label: '处理节点 B' }, - position: { x: 250, y: 200 }, - sourcePosition: Position.Bottom, - targetPosition: Position.Top, - }, +// { id: '1', type: 'inputNode', data: { label: '开始' }, position: { x: 100, y: 100 } }, +]; +const initialEdges: Edge[] = []; + +// --- 定义可拖拽的节点类型 --- +const nodeTypesForPalette = [ + { type: 'inputNode', label: '输入', icon: MessageSquareText, defaultData: { text: '用户输入...' } }, + { type: 'llmNode', label: 'LLM 调用', icon: BrainCircuit, defaultData: { model: 'gpt-3.5-turbo', prompt: '...' } }, + { type: 'ragNode', label: 'RAG 查询', icon: Database, defaultData: { query: '...', knowledgeBase: 'default' } }, + { type: 'outputNode', label: '输出', icon: LogOut, defaultData: { result: null } }, + { type: 'startNode', label: '开始流程', icon: Play, defaultData: {} }, // 添加一个开始节点类型 ]; -const initialEdges: Edge[] = [ - { - id: 'e1-2', - source: '1', // 源节点 ID - target: '2', // 目标节点 ID - label: '数据流 1', // 边标签 (可选) - markerEnd: { type: MarkerType.ArrowClosed }, // 箭头样式 - }, - { - id: 'e1-3', - source: '1', - target: '3', - label: '数据流 2', - markerEnd: { type: MarkerType.ArrowClosed }, - }, - { - id: 'e2-4', - source: '2', - target: '4', - animated: true, // 动画边 (可选) - label: '处理 A -> B', - markerEnd: { type: MarkerType.ArrowClosed }, - }, -]; +// --- 侧边栏组件 --- +const Sidebar = () => { + // 拖拽开始时设置数据 + const onDragStart = (event: DragEvent, nodeType: string, defaultData: any) => { + // 将节点类型和默认数据存储在 dataTransfer 中 + const nodeInfo = JSON.stringify({ nodeType, defaultData }); + event.dataTransfer.setData('application/reactflow', nodeInfo); + event.dataTransfer.effectAllowed = 'move'; + console.log(`Drag Start: ${nodeType}`); + }; -// --- 工作流页面组件 --- + return ( + + ); +}; + + +// --- 工作流编辑器组件 --- function WorkflowEditor() { - // 使用 React Flow 提供的 Hooks 管理节点和边的状态 + const reactFlowWrapper = useRef(null); // Ref 指向 React Flow 容器 const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); + const { project } = useReactFlow(); // 使用 hook 获取 project 方法 - // 当节点拖动、选择等交互发生时,更新节点状态 const onNodesChange = useCallback( (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), [setNodes] ); - - // 当边发生变化(如删除)时,更新边状态 const onEdgesChange = useCallback( (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), [setEdges] ); - - // 当用户拖动创建连接时触发 const onConnect = useCallback( - (connection: Connection) => { - // 创建新边 - const newEdge = { - ...connection, - id: `e-${connection.source}-${connection.target}-${Math.random()}`, // 简单生成 ID - markerEnd: { type: MarkerType.ArrowClosed }, // 添加箭头 - // animated: true, // 可以给新连接添加动画 - }; - setEdges((eds) => addEdge(newEdge, eds)); - console.log('连接成功:', connection); - }, + (connection: Connection) => setEdges((eds) => addEdge({ ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, eds)), [setEdges] ); - // --- (可选) 自定义节点类型 --- + // 处理画布上的拖拽悬停事件 + 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 reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); + const position = project({ + x: event.clientX - reactFlowBounds.left, + y: event.clientY - reactFlowBounds.top, + }); + + // 创建新节点 + const newNode: Node = { + id: `${nodeType}-${Date.now()}`, // 使用更唯一的 ID + type: nodeType, // 设置节点类型 + position, + data: { label: `${nodeType} node`, ...defaultData }, // 合并默认数据 + // 根据节点类型设置 Handle 位置 (可选,也可以在自定义节点内部定义) + sourcePosition: Position.Bottom, + targetPosition: Position.Top, + }; + + console.log('Node dropped:', newNode); + // 将新节点添加到状态中 + setNodes((nds) => nds.concat(newNode)); + }, + [project, setNodes] // 依赖 project 方法和 setNodes + ); + + // --- (可选) 注册自定义节点类型 --- // const nodeTypes = useMemo(() => ({ - // customInput: CustomInputNode, // 示例 - // llmNode: LLMNode, // 示例 + // inputNode: CustomInputNode, + // llmNode: LLMNode, + // ragNode: RAGNode, + // outputNode: CustomOutputNode, + // startNode: StartNode, // }), []); return ( @@ -131,30 +173,29 @@ function WorkflowEditor() { 工作流编辑器 - {/* 设置 React Flow 画布容器的高度 */} - {/* 减去标题栏高度 */} - - {/* 添加控件 */} - - - {/* 使用点状背景 */} - - {/* TODO: 添加侧边栏用于拖放节点 */} - {/* - 节点面板 - 拖拽节点 A - 拖拽节点 B - */} - + {/* 主容器使用 Flexbox */} + + {/* 侧边栏 */} + + {/* React Flow 画布容器 */} + {/* 添加 ref */} + + + + + + ); @@ -163,8 +204,6 @@ function WorkflowEditor() { // --- 主页面组件,包含 Provider --- export default function WorkflowPage() { return ( - // ReactFlowProvider 需要包裹使用 useReactFlow hook 的组件 - // 如果 WorkflowEditor 内部需要使用 useReactFlow,则 Provider 必须在外层
节点面板