| |
| |
| |
|
|
| 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; |