agent / frontend /src /components /WorkflowEditor.jsx
samlax12's picture
Upload 139 files
ad74240 verified
/**
* 工作流编辑器组件
* 使用React Flow实现可视化工作流编辑
*/
import { useState, useCallback, useRef } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
useNodesState,
useEdgesState,
Controls,
MiniMap,
Background,
Panel,
MarkerType
} from 'reactflow';
import 'reactflow/dist/style.css';
import {
Card,
Button,
Drawer,
Form,
Input,
Select,
Space,
Typography,
message,
Modal,
Divider,
Tag
} from 'antd';
import {
PlusOutlined,
SaveOutlined,
ClearOutlined,
PlayCircleOutlined,
QuestionCircleOutlined,
CodeOutlined,
FileTextOutlined,
BranchesOutlined,
CheckCircleOutlined
} from '@ant-design/icons';
const { Title, Text } = Typography;
const { TextArea } = Input;
const { Option } = Select;
// 节点类型定义
const nodeTypes = {
start: {
label: '开始节点',
icon: <PlayCircleOutlined />,
color: '#52c41a'
},
question: {
label: '提问节点',
icon: <QuestionCircleOutlined />,
color: '#1890ff'
},
knowledge: {
label: '知识查询',
icon: <FileTextOutlined />,
color: '#722ed1'
},
code: {
label: '代码执行',
icon: <CodeOutlined />,
color: '#fa8c16'
},
condition: {
label: '条件分支',
icon: <BranchesOutlined />,
color: '#f5222d'
},
end: {
label: '结束节点',
icon: <CheckCircleOutlined />,
color: '#52c41a'
}
};
// 自定义节点组件
const CustomNode = ({ data, selected }) => {
const nodeType = nodeTypes[data.type] || nodeTypes.question;
return (
<div
style={{
padding: '10px 15px',
borderRadius: '8px',
background: '#fff',
border: `2px solid ${selected ? nodeType.color : '#d9d9d9'}`,
boxShadow: selected ? `0 0 0 2px ${nodeType.color}20` : '0 2px 4px rgba(0,0,0,0.1)',
minWidth: '150px',
textAlign: 'center'
}}
>
<Space>
<span style={{ color: nodeType.color, fontSize: '16px' }}>
{nodeType.icon}
</span>
<Text strong>{data.label}</Text>
</Space>
{data.description && (
<div style={{ marginTop: '4px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
{data.description}
</Text>
</div>
)}
</div>
);
};
// 初始节点
const initialNodes = [
{
id: 'start',
type: 'custom',
position: { x: 250, y: 50 },
data: {
type: 'start',
label: '开始',
description: '工作流起点'
}
}
];
const WorkflowEditor = ({ value, onChange, onSave }) => {
const [nodes, setNodes, onNodesChange] = useNodesState(value?.nodes || initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(value?.edges || []);
const [drawerVisible, setDrawerVisible] = useState(false);
const [selectedNode, setSelectedNode] = useState(null);
const [nodeForm] = Form.useForm();
const reactFlowWrapper = useRef(null);
const reactFlowInstance = useRef(null);
// 连接节点
const onConnect = useCallback(
(params) => {
const newEdge = {
...params,
type: 'smoothstep',
animated: true,
style: { stroke: '#1890ff', strokeWidth: 2 },
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#1890ff'
}
};
setEdges((eds) => addEdge(newEdge, eds));
},
[setEdges]
);
// 添加节点
const addNode = (type) => {
const newNode = {
id: `${type}_${Date.now()}`,
type: 'custom',
position: {
x: Math.random() * 400 + 100,
y: Math.random() * 300 + 100
},
data: {
type,
label: nodeTypes[type].label,
description: '',
config: {}
}
};
setNodes((nds) => nds.concat(newNode));
message.success(`已添加${nodeTypes[type].label}`);
};
// 选中节点
const onNodeClick = (event, node) => {
setSelectedNode(node);
nodeForm.setFieldsValue({
label: node.data.label,
description: node.data.description,
...node.data.config
});
setDrawerVisible(true);
};
// 删除节点
const deleteNode = (nodeId) => {
setNodes((nds) => nds.filter((n) => n.id !== nodeId));
setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
message.success('节点已删除');
};
// 更新节点
const updateNode = (values) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id === selectedNode.id) {
return {
...node,
data: {
...node.data,
label: values.label,
description: values.description,
config: {
...values,
label: undefined,
description: undefined
}
}
};
}
return node;
})
);
setDrawerVisible(false);
message.success('节点已更新');
};
// 清空画布
const clearCanvas = () => {
Modal.confirm({
title: '确认清空',
content: '确定要清空所有节点和连接吗?',
onOk: () => {
setNodes([initialNodes[0]]);
setEdges([]);
message.success('画布已清空');
}
});
};
// 保存工作流
const handleSave = () => {
const workflow = {
nodes,
edges,
timestamp: Date.now()
};
if (onChange) {
onChange(workflow);
}
if (onSave) {
onSave(workflow);
}
message.success('工作流已保存');
};
// 验证工作流
const validateWorkflow = () => {
if (nodes.length < 2) {
message.warning('工作流至少需要包含开始和结束节点');
return false;
}
const hasStart = nodes.some(n => n.data.type === 'start');
const hasEnd = nodes.some(n => n.data.type === 'end');
if (!hasStart || !hasEnd) {
message.warning('工作流必须包含开始和结束节点');
return false;
}
return true;
};
return (
<div style={{ width: '100%', height: '600px' }}>
<ReactFlowProvider>
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onInit={(instance) => { reactFlowInstance.current = instance; }}
nodeTypes={{ custom: CustomNode }}
fitView
>
<Controls />
<MiniMap
nodeColor={node => nodeTypes[node.data.type]?.color || '#ccc'}
style={{
height: 120,
background: '#f0f2f5'
}}
/>
<Background variant="dots" gap={12} size={1} />
{/* 工具面板 */}
<Panel position="top-left">
<Card size="small" style={{ minWidth: '200px' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<Text strong>添加节点</Text>
<Space wrap>
{Object.entries(nodeTypes).map(([key, type]) => (
<Button
key={key}
size="small"
icon={type.icon}
onClick={() => addNode(key)}
style={{ fontSize: '12px' }}
>
{type.label}
</Button>
))}
</Space>
</Space>
</Card>
</Panel>
{/* 操作按钮 */}
<Panel position="top-right">
<Space>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!validateWorkflow()}
>
保存工作流
</Button>
<Button
danger
icon={<ClearOutlined />}
onClick={clearCanvas}
>
清空画布
</Button>
</Space>
</Panel>
</ReactFlow>
</div>
</ReactFlowProvider>
{/* 节点编辑抽屉 */}
<Drawer
title="编辑节点"
placement="right"
width={400}
open={drawerVisible}
onClose={() => setDrawerVisible(false)}
footer={
<Space>
<Button onClick={() => setDrawerVisible(false)}>取消</Button>
<Button
danger
onClick={() => {
deleteNode(selectedNode.id);
setDrawerVisible(false);
}}
>
删除节点
</Button>
<Button type="primary" onClick={() => nodeForm.submit()}>
保存
</Button>
</Space>
}
>
{selectedNode && (
<Form
form={nodeForm}
layout="vertical"
onFinish={updateNode}
>
<Form.Item
name="label"
label="节点名称"
rules={[{ required: true, message: '请输入节点名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="description"
label="节点描述"
>
<TextArea rows={2} />
</Form.Item>
<Divider />
{/* 根据节点类型显示不同的配置项 */}
{selectedNode.data.type === 'question' && (
<Form.Item
name="prompt"
label="提示词模板"
>
<TextArea rows={4} placeholder="输入提示词模板..." />
</Form.Item>
)}
{selectedNode.data.type === 'knowledge' && (
<Form.Item
name="knowledge_base"
label="知识库"
>
<Select placeholder="选择知识库">
<Option value="default">默认知识库</Option>
</Select>
</Form.Item>
)}
{selectedNode.data.type === 'code' && (
<Form.Item
name="code"
label="代码"
>
<TextArea rows={6} placeholder="输入Python代码..." style={{ fontFamily: 'monospace' }} />
</Form.Item>
)}
{selectedNode.data.type === 'condition' && (
<>
<Form.Item
name="condition"
label="条件表达式"
>
<Input placeholder="例如: score > 80" />
</Form.Item>
<Form.Item
name="true_branch"
label="满足条件时"
>
<Input placeholder="跳转到节点..." />
</Form.Item>
<Form.Item
name="false_branch"
label="不满足条件时"
>
<Input placeholder="跳转到节点..." />
</Form.Item>
</>
)}
</Form>
)}
</Drawer>
</div>
);
};
export default WorkflowEditor;