2025-05-01 13:11:21 +08:00

212 lines
7.5 KiB
TypeScript

// File: frontend/app/workflow/page.tsx
// Description: 工作流编辑器页面,添加侧边栏用于拖放节点
'use client';
import React, { useState, useCallback, useRef, DragEvent } from 'react'; // 添加 useRef, DragEvent
import ReactFlow, {
ReactFlowProvider,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Node,
Edge,
Connection,
Position,
MarkerType,
NodeChange,
EdgeChange,
applyNodeChanges,
applyEdgeChanges,
useReactFlow, // 导入 useReactFlow hook
XYPosition,
BackgroundVariant, // 导入 XYPosition 类型
} from 'reactflow';
import { MessageSquareText, BrainCircuit, Database, LogOut, Play } from 'lucide-react'; // 导入一些节点图标
import 'reactflow/dist/style.css';
// --- 初始节点和边数据 (可以清空或保留示例) ---
const initialNodes: Node[] = [
// { 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 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 (
<aside className="w-64 bg-white dark:bg-gray-800 p-4 border-r dark:border-gray-700 shadow-md overflow-y-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-200"></h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"></p>
<div className="space-y-2">
{nodeTypesForPalette.map((nodeInfo) => (
<div
key={nodeInfo.type}
className="p-3 border dark:border-gray-600 rounded-lg cursor-grab flex items-center gap-2 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
onDragStart={(event) => onDragStart(event, nodeInfo.type, nodeInfo.defaultData)}
draggable // 设置为可拖拽
>
<nodeInfo.icon size={18} className="text-gray-600 dark:text-gray-300 flex-shrink-0" />
<span className="text-sm text-gray-700 dark:text-gray-200">{nodeInfo.label}</span>
</div>
))}
</div>
</aside>
);
};
// --- 工作流编辑器组件 ---
function WorkflowEditor() {
const reactFlowWrapper = useRef<HTMLDivElement>(null); // Ref 指向 React Flow 容器
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(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) => 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(() => ({
// inputNode: CustomInputNode,
// llmNode: LLMNode,
// ragNode: RAGNode,
// outputNode: CustomOutputNode,
// startNode: StartNode,
// }), []);
return (
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<h1 className="text-xl font-semibold p-4 border-b dark:border-gray-700 text-gray-800 dark:text-gray-200">
</h1>
{/* 主容器使用 Flexbox */}
<div className="flex flex-1 overflow-hidden">
{/* 侧边栏 */}
<Sidebar />
{/* React Flow 画布容器 */}
<div className="flex-1 h-full" ref={reactFlowWrapper}> {/* 添加 ref */}
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDragOver={onDragOver} // 添加 onDragOver
onDrop={onDrop} // 添加 onDrop
// nodeTypes={nodeTypes}
fitView
className="bg-gray-50 dark:bg-gray-900"
>
<Controls />
<MiniMap nodeStrokeWidth={3} zoomable pannable />
<Background gap={16} color="#ccc" variant={BackgroundVariant.Dots} />
</ReactFlow>
</div>
</div>
</div>
);
}
// --- 主页面组件,包含 Provider ---
export default function WorkflowPage() {
return (
<ReactFlowProvider>
<WorkflowEditor />
</ReactFlowProvider>
);
}