import type React from "react"; import { useState, useCallback } from "react"; import { ReactFlow, MiniMap, Controls, Background, useNodesState, useEdgesState, addEdge, type Connection, type Node, type NodeTypes, BackgroundVariant, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { Button, Card, Input, Badge, Typography } from "antd"; import TextArea from "antd/es/input/TextArea"; import { Play, Save, ArrowLeft, Database, Download, Bug, Search, MessageSquare, Cpu, } from "lucide-react"; import CustomNode from "./components/CustomNode"; const { Title } = Typography; const nodeTypes: NodeTypes = { customNode: CustomNode, }; interface WorkflowEditorProps { onBack: () => void; onSave: (workflow: any) => void; initialWorkflow?: any; } const nodeTypeTemplates = [ { type: "knowledge-search", name: "知识库搜索", description: "查询、过滤和检索知识库中的文档内容,为AI模型提供上下文信息", icon: Database, category: "数据源", inputs: 1, outputs: 1, }, { type: "ai-dialogue", name: "AI 对话", description: "AI 大模型对话", icon: MessageSquare, category: "AI处理", inputs: 1, outputs: 1, }, { type: "data-processing", name: "数据处理", description: "对数据进行清洗、转换和处理", icon: Cpu, category: "数据处理", inputs: 1, outputs: 1, }, { type: "data-output", name: "数据输出", description: "将处理后的数据输出到指定位置", icon: Download, category: "数据输出", inputs: 1, outputs: 0, }, ]; export default function WorkflowEditor({ onBack, onSave, initialWorkflow, }: WorkflowEditorProps) { const [workflow, setWorkflow] = useState({ id: initialWorkflow?.id || Date.now(), name: initialWorkflow?.name || "新建流程", description: initialWorkflow?.description || "描述您的数据处理流程", category: initialWorkflow?.category || "自定义", }); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedNodeId, setSelectedNodeId] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const filteredNodeTypes = nodeTypeTemplates.filter( (nodeType) => nodeType.name.toLowerCase().includes(searchTerm.toLowerCase()) || nodeType.description.toLowerCase().includes(searchTerm.toLowerCase()) ); const onConnect = useCallback( (params: Connection) => { setEdges((eds) => addEdge(params, eds)); }, [setEdges] ); const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => { setSelectedNodeId(node.id); }, []); const onPaneClick = useCallback(() => { setSelectedNodeId(null); }, []); const onDragStart = (event: React.DragEvent, nodeType: string) => { event.dataTransfer.setData("application/reactflow", nodeType); event.dataTransfer.effectAllowed = "move"; }; const deleteNode = useCallback( (nodeId: string) => { setNodes((nds) => nds.filter((node) => node.id !== nodeId)); setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId) ); }, [setNodes, setEdges] ); const duplicateNode = useCallback( (nodeId: string) => { const nodeToDuplicate = nodes.find((node) => node.id === nodeId); if (!nodeToDuplicate) return; const newNode: Node = { ...nodeToDuplicate, id: `${nodeToDuplicate.data.type}_${Date.now()}`, position: { x: nodeToDuplicate.position.x + 50, y: nodeToDuplicate.position.y + 50, }, data: { ...nodeToDuplicate.data, id: `${nodeToDuplicate.data.type}_${Date.now()}`, }, }; setNodes((nds) => nds.concat(newNode)); }, [nodes, setNodes] ); const handleSave = () => { const workflowData = { ...workflow, nodes: nodes.map((node) => ({ id: node.id, type: node.data.type, name: node.data.name, description: node.data.description, position: node.position, config: node.data.config || {}, })), connections: edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, })), }; onSave(workflowData); }; const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const type = event.dataTransfer.getData("application/reactflow"); if (typeof type === "undefined" || !type) { return; } const position = { x: event.clientX - 400, // Adjust for sidebar width y: event.clientY - 100, // Adjust for header height }; const nodeTemplate = nodeTypeTemplates.find( (template) => template.type === type ); if (!nodeTemplate) return; const newNode: Node = { id: `${type}_${Date.now()}`, type: "customNode", position, data: { id: `${type}_${Date.now()}`, type: type, name: nodeTemplate.name, description: nodeTemplate.description, onDelete: deleteNode, onDuplicate: duplicateNode, }, }; setNodes((nds) => nds.concat(newNode)); }, [setNodes, deleteNode, duplicateNode] ); return (
{/* Header */}
setWorkflow((prev) => ({ ...prev, name: e.target.value })) } className="text-lg font-semibold border-none p-0 h-auto bg-transparent focus-visible:ring-0" placeholder="流程名称" bordered={false} /> setWorkflow((prev) => ({ ...prev, description: e.target.value, })) } className="text-sm text-gray-600 border-none p-0 h-auto bg-transparent focus-visible:ring-0 mt-1" placeholder="流程描述" bordered={false} />
{/* Component Library Sidebar */}
setSearchTerm(e.target.value)} className="pl-10" />
{filteredNodeTypes.map((nodeType) => ( onDragStart(event, nodeType.type)} styles={{ body: { padding: 16 } }} >
{nodeType.name}
{nodeType.description}
{nodeType.category}
))}
{/* Main Canvas */}
connection.source !== connection.target } >
{/* Properties Panel */} {selectedNodeId && (
节点配置
{(() => { const selectedNode = nodes.find( (node) => node.id === selectedNodeId ); if (!selectedNode) return null; return ( <>
{ setNodes((nds) => nds.map((node) => node.id === selectedNode.id ? { ...node, data: { ...node.data, name: e.target.value, }, } : node ) ); }} className="mt-1" />