272 lines
9.7 KiB
TypeScript
272 lines
9.7 KiB
TypeScript
// 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<HTMLDivElement>(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 (
|
|
<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">
|
|
{/* 侧边栏 */}
|
|
<WorkflowSidebar />
|
|
{/* React Flow 画布容器 */}
|
|
<div className="flex-1 h-full" ref={reactFlowWrapper}> {/* 添加 ref */}
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChangeIntercepted}
|
|
onConnect={onConnect}
|
|
onDragOver={onDragOver} // 添加 onDragOver
|
|
onDrop={onDrop} // 添加 onDrop
|
|
nodeTypes={nodeTypes} // <--- 传递自定义节点类型
|
|
fitView
|
|
className="bg-gray-50 dark:bg-gray-900"
|
|
>
|
|
{/* 使用 Panel 添加运行按钮到右上角 */}
|
|
<Panel position="top-right" className="p-2">
|
|
<Button
|
|
onClick={handleRunWorkflow}
|
|
disabled={isRunning}
|
|
size="sm"
|
|
className="bg-green-600 hover:bg-green-700 text-white"
|
|
>
|
|
{isRunning ? (
|
|
<LoaderIcon className="mr-2 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<PlayIcon className="mr-2 h-4 w-4" />
|
|
)}
|
|
运行
|
|
</Button>
|
|
</Panel>
|
|
<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>
|
|
);
|
|
}
|