创建节点侧边栏
This commit is contained in:
parent
b57b2114cd
commit
37bdf4dbfd
@ -1,129 +1,171 @@
|
|||||||
// File: frontend/app/workflow/page.tsx
|
// 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, {
|
import ReactFlow, {
|
||||||
ReactFlowProvider, // Provider 包裹应用
|
ReactFlowProvider,
|
||||||
MiniMap, // 小地图
|
MiniMap,
|
||||||
Controls, // 控制按钮 (缩放, 适应视图)
|
Controls,
|
||||||
Background, // 背景网格
|
Background,
|
||||||
useNodesState, // Hook 管理节点状态
|
useNodesState,
|
||||||
useEdgesState, // Hook 管理边状态
|
useEdgesState,
|
||||||
addEdge, // Helper 函数添加边
|
addEdge,
|
||||||
Node, // 节点类型
|
Node,
|
||||||
Edge, // 边类型
|
Edge,
|
||||||
Connection, // 连接类型
|
Connection,
|
||||||
Position, // 用于 Handle 定位
|
Position,
|
||||||
MarkerType, // 用于边箭头类型
|
MarkerType,
|
||||||
NodeChange, // 节点变化类型
|
NodeChange,
|
||||||
EdgeChange, // 边变化类型
|
EdgeChange,
|
||||||
applyNodeChanges, // 应用节点变化
|
applyNodeChanges,
|
||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
BackgroundVariant, // 应用边变化
|
useReactFlow, // 导入 useReactFlow hook
|
||||||
|
XYPosition,
|
||||||
|
BackgroundVariant, // 导入 XYPosition 类型
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
import { MessageSquareText, BrainCircuit, Database, LogOut, Play } from 'lucide-react'; // 导入一些节点图标
|
||||||
|
|
||||||
// 引入 React Flow 的 CSS 样式
|
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
|
|
||||||
// --- 初始节点和边数据 (示例) ---
|
// --- 初始节点和边数据 (可以清空或保留示例) ---
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
// { id: '1', type: 'inputNode', data: { label: '开始' }, position: { x: 100, y: 100 } },
|
||||||
id: '1',
|
];
|
||||||
type: 'input', // React Flow 内建输入类型节点
|
const initialEdges: Edge[] = [];
|
||||||
data: { label: '开始节点' },
|
|
||||||
position: { x: 250, y: 5 },
|
// --- 定义可拖拽的节点类型 ---
|
||||||
sourcePosition: Position.Bottom, // Handle (连接点) 位置
|
const nodeTypesForPalette = [
|
||||||
},
|
{ type: 'inputNode', label: '输入', icon: MessageSquareText, defaultData: { text: '用户输入...' } },
|
||||||
{
|
{ type: 'llmNode', label: 'LLM 调用', icon: BrainCircuit, defaultData: { model: 'gpt-3.5-turbo', prompt: '...' } },
|
||||||
id: '2',
|
{ type: 'ragNode', label: 'RAG 查询', icon: Database, defaultData: { query: '...', knowledgeBase: 'default' } },
|
||||||
// type: 'default', // 默认类型节点
|
{ type: 'outputNode', label: '输出', icon: LogOut, defaultData: { result: null } },
|
||||||
data: { label: '处理节点 A' },
|
{ type: 'startNode', label: '开始流程', icon: Play, defaultData: {} }, // 添加一个开始节点类型
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
// --- 侧边栏组件 ---
|
||||||
{
|
const Sidebar = () => {
|
||||||
id: 'e1-2',
|
// 拖拽开始时设置数据
|
||||||
source: '1', // 源节点 ID
|
const onDragStart = (event: DragEvent, nodeType: string, defaultData: any) => {
|
||||||
target: '2', // 目标节点 ID
|
// 将节点类型和默认数据存储在 dataTransfer 中
|
||||||
label: '数据流 1', // 边标签 (可选)
|
const nodeInfo = JSON.stringify({ nodeType, defaultData });
|
||||||
markerEnd: { type: MarkerType.ArrowClosed }, // 箭头样式
|
event.dataTransfer.setData('application/reactflow', nodeInfo);
|
||||||
},
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
{
|
console.log(`Drag Start: ${nodeType}`);
|
||||||
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 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- 工作流页面组件 ---
|
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() {
|
function WorkflowEditor() {
|
||||||
// 使用 React Flow 提供的 Hooks 管理节点和边的状态
|
const reactFlowWrapper = useRef<HTMLDivElement>(null); // Ref 指向 React Flow 容器
|
||||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||||
|
const { project } = useReactFlow(); // 使用 hook 获取 project 方法
|
||||||
|
|
||||||
// 当节点拖动、选择等交互发生时,更新节点状态
|
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||||
[setNodes]
|
[setNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 当边发生变化(如删除)时,更新边状态
|
|
||||||
const onEdgesChange = useCallback(
|
const onEdgesChange = useCallback(
|
||||||
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||||
[setEdges]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 当用户拖动创建连接时触发
|
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
(connection: Connection) => {
|
(connection: Connection) => setEdges((eds) => addEdge({ ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, eds)),
|
||||||
// 创建新边
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
[setEdges]
|
[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(() => ({
|
// const nodeTypes = useMemo(() => ({
|
||||||
// customInput: CustomInputNode, // 示例
|
// inputNode: CustomInputNode,
|
||||||
// llmNode: LLMNode, // 示例
|
// llmNode: LLMNode,
|
||||||
|
// ragNode: RAGNode,
|
||||||
|
// outputNode: CustomOutputNode,
|
||||||
|
// startNode: StartNode,
|
||||||
// }), []);
|
// }), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -131,30 +173,29 @@ function WorkflowEditor() {
|
|||||||
<h1 className="text-xl font-semibold p-4 border-b dark:border-gray-700 text-gray-800 dark:text-gray-200">
|
<h1 className="text-xl font-semibold p-4 border-b dark:border-gray-700 text-gray-800 dark:text-gray-200">
|
||||||
工作流编辑器
|
工作流编辑器
|
||||||
</h1>
|
</h1>
|
||||||
{/* 设置 React Flow 画布容器的高度 */}
|
{/* 主容器使用 Flexbox */}
|
||||||
<div style={{ height: 'calc(100% - 65px)' }} className="flex-1"> {/* 减去标题栏高度 */}
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<ReactFlow
|
{/* 侧边栏 */}
|
||||||
nodes={nodes}
|
<Sidebar />
|
||||||
edges={edges}
|
{/* React Flow 画布容器 */}
|
||||||
onNodesChange={onNodesChange}
|
<div className="flex-1 h-full" ref={reactFlowWrapper}> {/* 添加 ref */}
|
||||||
onEdgesChange={onEdgesChange}
|
<ReactFlow
|
||||||
onConnect={onConnect}
|
nodes={nodes}
|
||||||
// nodeTypes={nodeTypes} // 注册自定义节点类型
|
edges={edges}
|
||||||
fitView // 初始加载时适应视图
|
onNodesChange={onNodesChange}
|
||||||
className="bg-gray-50 dark:bg-gray-900" // 设置画布背景色
|
onEdgesChange={onEdgesChange}
|
||||||
>
|
onConnect={onConnect}
|
||||||
{/* 添加控件 */}
|
onDragOver={onDragOver} // 添加 onDragOver
|
||||||
<Controls />
|
onDrop={onDrop} // 添加 onDrop
|
||||||
<MiniMap nodeStrokeWidth={3} zoomable pannable />
|
// nodeTypes={nodeTypes}
|
||||||
<Background gap={16} color="#ccc" variant={BackgroundVariant.Dots} /> {/* 使用点状背景 */}
|
fitView
|
||||||
|
className="bg-gray-50 dark:bg-gray-900"
|
||||||
{/* TODO: 添加侧边栏用于拖放节点 */}
|
>
|
||||||
{/* <div className="absolute left-4 top-20 z-10 bg-white dark:bg-gray-700 p-4 rounded shadow">
|
<Controls />
|
||||||
<p>节点面板</p>
|
<MiniMap nodeStrokeWidth={3} zoomable pannable />
|
||||||
<button className="p-2 border rounded mt-2 block">拖拽节点 A</button>
|
<Background gap={16} color="#ccc" variant={BackgroundVariant.Dots} />
|
||||||
<button className="p-2 border rounded mt-2 block">拖拽节点 B</button>
|
</ReactFlow>
|
||||||
</div> */}
|
</div>
|
||||||
</ReactFlow>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -163,8 +204,6 @@ function WorkflowEditor() {
|
|||||||
// --- 主页面组件,包含 Provider ---
|
// --- 主页面组件,包含 Provider ---
|
||||||
export default function WorkflowPage() {
|
export default function WorkflowPage() {
|
||||||
return (
|
return (
|
||||||
// ReactFlowProvider 需要包裹使用 useReactFlow hook 的组件
|
|
||||||
// 如果 WorkflowEditor 内部需要使用 useReactFlow,则 Provider 必须在外层
|
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<WorkflowEditor />
|
<WorkflowEditor />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user