init datamate

This commit is contained in:
Dallas98
2025-10-21 23:00:48 +08:00
commit 1c97afed7d
692 changed files with 135442 additions and 0 deletions

View File

@@ -0,0 +1,842 @@
import React, { useState } from "react";
import { Button, Card, Progress, Badge, Tabs } from "antd";
import {
GitBranch,
Play,
Pause,
Square,
Download,
Upload,
Plus,
Settings,
Database,
Filter,
Shuffle,
Target,
Zap,
Clock,
CheckCircle,
AlertCircle,
XCircle,
Eye,
Copy,
Edit,
ArrowRight,
ArrowLeft,
} from "lucide-react";
import { useNavigate } from "react-router";
import DevelopmentInProgress from "@/components/DevelopmentInProgress.tsx";
const { TabPane } = Tabs;
interface FlowNode {
id: string;
type: string;
name: string;
description: string;
position: { x: number; y: number };
config: any;
status: "idle" | "running" | "completed" | "error";
progress?: number;
}
interface FlowConnection {
id: string;
source: string;
target: string;
}
interface FlowTemplate {
id: number;
name: string;
description: string;
category: string;
nodes: FlowNode[];
connections: FlowConnection[];
createdAt: string;
lastUsed?: string;
usageCount: number;
}
interface FlowExecution {
id: number;
templateName: string;
status: "running" | "completed" | "failed" | "paused";
progress: number;
startTime: string;
endTime?: string;
duration?: string;
processedRecords: number;
totalRecords: number;
}
const nodeTypes = [
{
type: "data-source",
name: "数据源",
icon: Database,
description: "从各种数据源读取数据",
color: "bg-blue-500",
category: "输入",
},
{
type: "data-filter",
name: "数据过滤",
icon: Filter,
description: "根据条件过滤数据",
color: "bg-green-500",
category: "处理",
},
{
type: "data-transform",
name: "数据转换",
icon: Shuffle,
description: "转换数据格式和结构",
color: "bg-purple-500",
category: "处理",
},
{
type: "data-validation",
name: "数据验证",
icon: Target,
description: "验证数据质量和完整性",
color: "bg-orange-500",
category: "处理",
},
{
type: "data-enhancement",
name: "数据增强",
icon: Zap,
description: "增强和丰富数据内容",
color: "bg-pink-500",
category: "处理",
},
{
type: "data-output",
name: "数据输出",
icon: Download,
description: "将处理后的数据输出到目标位置",
color: "bg-indigo-500",
category: "输出",
},
];
const mockTemplates: FlowTemplate[] = [
{
id: 1,
name: "WSI病理图像预处理流程",
description: "专用于WSI病理图像的标准化预处理流程",
category: "医学影像",
nodes: [
{
id: "node1",
type: "data-source",
name: "WSI图像源",
description: "读取WSI病理图像",
position: { x: 100, y: 100 },
config: { source: "wsi_pathology", format: "svs" },
status: "idle",
},
{
id: "node2",
type: "data-validation",
name: "图像质量检查",
description: "检查图像质量和完整性",
position: { x: 300, y: 100 },
config: { minSize: "1GB", maxSize: "5GB" },
status: "idle",
},
{
id: "node3",
type: "data-transform",
name: "图像标准化",
description: "标准化图像格式和尺寸",
position: { x: 500, y: 100 },
config: { targetFormat: "tiff", normalize: true },
status: "idle",
},
{
id: "node4",
type: "data-output",
name: "处理结果输出",
description: "输出处理后的图像",
position: { x: 700, y: 100 },
config: { destination: "processed_wsi" },
status: "idle",
},
],
connections: [
{ id: "conn1", source: "node1", target: "node2" },
{ id: "conn2", source: "node2", target: "node3" },
{ id: "conn3", source: "node3", target: "node4" },
],
createdAt: "2024-01-20",
lastUsed: "2024-01-23",
usageCount: 15,
},
{
id: 2,
name: "文本数据清洗流程",
description: "通用文本数据清洗和标准化流程",
category: "文本处理",
nodes: [
{
id: "node1",
type: "data-source",
name: "文本数据源",
description: "读取原始文本数据",
position: { x: 100, y: 100 },
config: { source: "text_corpus", encoding: "utf-8" },
status: "idle",
},
{
id: "node2",
type: "data-filter",
name: "内容过滤",
description: "过滤无效和重复内容",
position: { x: 300, y: 100 },
config: { minLength: 10, removeDuplicates: true },
status: "idle",
},
{
id: "node3",
type: "data-enhancement",
name: "文本增强",
description: "文本清洗和格式化",
position: { x: 500, y: 100 },
config: { removeHtml: true, normalizeWhitespace: true },
status: "idle",
},
{
id: "node4",
type: "data-output",
name: "清洗结果输出",
description: "输出清洗后的文本",
position: { x: 700, y: 100 },
config: { format: "jsonl" },
status: "idle",
},
],
connections: [
{ id: "conn1", source: "node1", target: "node2" },
{ id: "conn2", source: "node2", target: "node3" },
{ id: "conn3", source: "node3", target: "node4" },
],
createdAt: "2024-01-18",
lastUsed: "2024-01-22",
usageCount: 28,
},
];
const mockExecutions: FlowExecution[] = [
{
id: 1,
templateName: "WSI病理图像预处理流程",
status: "running",
progress: 65,
startTime: "2024-01-23 14:30:00",
processedRecords: 650,
totalRecords: 1000,
},
{
id: 2,
templateName: "文本数据清洗流程",
status: "completed",
progress: 100,
startTime: "2024-01-23 10:15:00",
endTime: "2024-01-23 12:45:00",
duration: "2h 30m",
processedRecords: 50000,
totalRecords: 50000,
},
{
id: 3,
templateName: "WSI病理图像预处理流程",
status: "failed",
progress: 25,
startTime: "2024-01-22 16:20:00",
endTime: "2024-01-22 16:45:00",
duration: "25m",
processedRecords: 250,
totalRecords: 1000,
},
];
export default function OrchestrationPage() {
return <DevelopmentInProgress />;
const navigate = useNavigate();
const [templates, setTemplates] = useState<FlowTemplate[]>(mockTemplates);
const [executions, setExecutions] = useState<FlowExecution[]>(mockExecutions);
const [selectedTemplate, setSelectedTemplate] = useState<FlowTemplate | null>(
null
);
const [showWorkflowEditor, setShowWorkflowEditor] = useState(false);
const startNewFlow = () => {
setShowWorkflowEditor(true);
};
const handleSaveWorkflow = (workflow: any) => {
setTemplates([workflow, ...templates]);
setShowWorkflowEditor(false);
};
const handleBackFromEditor = () => {
setShowWorkflowEditor(false);
};
if (showWorkflowEditor) {
const WorkflowEditor = React.lazy(() => import("./WorkflowEditor.tsx"));
return (
<React.Suspense fallback={<div>Loading...</div>}>
<WorkflowEditor
onBack={handleBackFromEditor}
onSave={handleSaveWorkflow}
/>
</React.Suspense>
);
}
const executeTemplate = (template: FlowTemplate) => {
const newExecution: FlowExecution = {
id: Date.now(),
templateName: template.name,
status: "running",
progress: 0,
startTime: new Date().toLocaleString(),
processedRecords: 0,
totalRecords: 1000,
};
setExecutions([newExecution, ...executions]);
// 模拟执行进度
const interval = setInterval(() => {
setExecutions((prev) =>
prev.map((exec) => {
if (exec.id === newExecution.id) {
const newProgress = Math.min(
exec.progress + Math.random() * 10,
100
);
return {
...exec,
progress: newProgress,
status: newProgress >= 100 ? "completed" : "running",
processedRecords: Math.floor(
(newProgress / 100) * exec.totalRecords
),
endTime:
newProgress >= 100 ? new Date().toLocaleString() : undefined,
};
}
return exec;
})
);
}, 500);
setTimeout(() => clearInterval(interval), 10000);
};
const getStatusIcon = (status: string) => {
switch (status) {
case "running":
return <Clock className="w-4 h-4 text-blue-500" />;
case "completed":
return <CheckCircle className="w-4 h-4 text-green-500" />;
case "failed":
return <XCircle className="w-4 h-4 text-red-500" />;
case "paused":
return <Pause className="w-4 h-4 text-yellow-500" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-500" />;
}
};
const getStatusBadge = (status: string) => {
const statusConfig = {
running: { label: "运行中", color: "processing" as const },
completed: { label: "已完成", color: "success" as const },
failed: { label: "失败", color: "error" as const },
paused: { label: "已暂停", color: "warning" as const },
};
return (
statusConfig[status as keyof typeof statusConfig] || statusConfig.running
);
};
const getNodeIcon = (nodeType: string) => {
const nodeTypeInfo = nodeTypes.find((nt) => nt.type === nodeType);
const IconComponent = nodeTypeInfo?.icon || Settings;
return <IconComponent className="w-4 h-4" />;
};
return (
<div className="space-y-4 p-4 bg-gray-50 min-h-screen">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
type="text"
size="small"
onClick={() => navigate(-1)}
icon={<ArrowLeft className="w-4 h-4 mr-2" />}
></Button>
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
</div>
</div>
<div className="flex gap-2">
<Button
onClick={() => setSelectedTemplate(null)}
icon={<Upload className="w-4 h-4 mr-2" />}
>
</Button>
<Button
type="primary"
onClick={startNewFlow}
icon={<Plus className="w-4 h-4 mr-2" />}
>
</Button>
</div>
</div>
{/* Main Content */}
<Tabs defaultActiveKey="templates">
<TabPane
tab={<span> ({templates.length})</span>}
key="templates"
>
<div className="grid gap-4">
{templates.map((template) => (
<Card
key={template.id}
className="hover:shadow-md transition-shadow"
>
<div className="pt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<GitBranch className="w-5 h-5 text-orange-600" />
</div>
<div>
<h4 className="font-semibold">{template.name}</h4>
<p className="text-sm text-gray-600">
{template.description}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge color="blue">{template.category}</Badge>
<Button
onClick={() => setSelectedTemplate(template)}
icon={<Eye className="w-4 h-4 mr-1" />}
>
</Button>
<Button
type="primary"
onClick={() => executeTemplate(template)}
icon={<Play className="w-4 h-4 mr-1" />}
>
</Button>
</div>
</div>
<div className="flex items-center gap-6 text-sm text-gray-600">
<div className="flex items-center gap-1">
<Settings className="w-4 h-4" />
<span>{template.nodes.length} </span>
</div>
<div className="flex items-center gap-1">
<ArrowRight className="w-4 h-4" />
<span>{template.connections.length} </span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span> {template.createdAt}</span>
</div>
<div className="flex items-center gap-1">
<Target className="w-4 h-4" />
<span>使 {template.usageCount} </span>
</div>
</div>
{/* Flow Preview */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-medium text-gray-700">
:
</span>
</div>
<div className="flex items-center gap-2 overflow-x-auto">
{template.nodes.map((node, index) => (
<div
key={node.id}
className="flex items-center gap-2 flex-shrink-0"
>
<div className="flex items-center gap-2 bg-white rounded px-3 py-1 border">
{getNodeIcon(node.type)}
<span className="text-xs font-medium">
{node.name}
</span>
</div>
{index < template.nodes.length - 1 && (
<ArrowRight className="w-4 h-4 text-gray-400" />
)}
</div>
))}
</div>
</div>
</div>
</div>
</Card>
))}
</div>
</TabPane>
<TabPane
tab={<span> ({executions.length})</span>}
key="executions"
>
<div className="grid gap-4">
{executions.map((execution) => (
<Card key={execution.id}>
<div className="pt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getStatusIcon(execution.status)}
<div>
<h4 className="font-semibold">
{execution.templateName}
</h4>
<p className="text-sm text-gray-600">
ID: {execution.id}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge status={getStatusBadge(execution.status).color}>
{getStatusBadge(execution.status).label}
</Badge>
{execution.status === "running" && (
<div className="flex gap-1">
<Button>
<Pause className="w-4 h-4" />
</Button>
<Button>
<Square className="w-4 h-4" />
</Button>
</div>
)}
</div>
</div>
{execution.status === "running" && (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span></span>
<span>
{execution.processedRecords.toLocaleString()} /{" "}
{execution.totalRecords.toLocaleString()}
</span>
</div>
<Progress percent={execution.progress} />
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-500">:</span>
<div className="font-medium">{execution.startTime}</div>
</div>
{execution.endTime && (
<div>
<span className="text-gray-500">:</span>
<div className="font-medium">{execution.endTime}</div>
</div>
)}
{execution.duration && (
<div>
<span className="text-gray-500">:</span>
<div className="font-medium">
{execution.duration}
</div>
</div>
)}
<div>
<span className="text-gray-500">:</span>
<div className="font-medium">
{execution.processedRecords.toLocaleString()}
</div>
</div>
</div>
</div>
</div>
</Card>
))}
</div>
</TabPane>
<TabPane tab={<span></span>} key="monitoring">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<div className="pt-6 flex items-center gap-2">
<Play className="w-5 h-5 text-blue-500" />
<div>
<p className="text-2xl font-bold">
{executions.filter((e) => e.status === "running").length}
</p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
</Card>
<Card>
<div className="pt-6 flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<div>
<p className="text-2xl font-bold">
{executions.filter((e) => e.status === "completed").length}
</p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
</Card>
<Card>
<div className="pt-6 flex items-center gap-2">
<XCircle className="w-5 h-5 text-red-500" />
<div>
<p className="text-2xl font-bold">
{executions.filter((e) => e.status === "failed").length}
</p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
</Card>
<Card>
<div className="pt-6 flex items-center gap-2">
<GitBranch className="w-5 h-5 text-purple-500" />
<div>
<p className="text-2xl font-bold">{templates.length}</p>
<p className="text-sm text-gray-500"></p>
</div>
</div>
</Card>
</div>
{/* Real-time Execution Monitor */}
<Card>
<div style={{ padding: 24 }}>
<h3></h3>
<div style={{ color: "#888", marginBottom: 16 }}>
</div>
{executions.filter((e) => e.status === "running").length > 0 ? (
<div className="space-y-4">
{executions
.filter((e) => e.status === "running")
.map((execution) => (
<div
key={execution.id}
className="border rounded-lg p-4 mb-4"
>
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium">
{execution.templateName}
</h4>
<Badge status="processing"></Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>: {Math.round(execution.progress)}%</span>
<span>
{execution.processedRecords.toLocaleString()} /{" "}
{execution.totalRecords.toLocaleString()}
</span>
</div>
<Progress percent={execution.progress} />
<div className="text-xs text-gray-500">
: {execution.startTime}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<Clock className="w-12 h-12 mx-auto mb-2 text-gray-400" />
<p></p>
</div>
)}
</div>
</Card>
</TabPane>
</Tabs>
{/* Template Detail Modal */}
{selectedTemplate && (
<Card style={{ border: "2px solid #91caff" }}>
<div style={{ padding: 24, borderBottom: "1px solid #f0f0f0" }}>
<div className="flex items-center justify-between">
<div>
<div
className="flex items-center gap-2"
style={{ fontSize: 20, fontWeight: 600 }}
>
<GitBranch className="w-5 h-5 text-orange-500" />
{selectedTemplate.name}
</div>
<div style={{ color: "#888" }}>
{selectedTemplate.description}
</div>
</div>
<div className="flex gap-2">
<Button icon={<Copy className="w-4 h-4 mr-1" />}></Button>
<Button icon={<Edit className="w-4 h-4 mr-1" />}></Button>
<Button
type="primary"
icon={<Play className="w-4 h-4 mr-1" />}
onClick={() => executeTemplate(selectedTemplate)}
>
</Button>
<Button onClick={() => setSelectedTemplate(null)}></Button>
</div>
</div>
</div>
<div style={{ padding: 24 }}>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div
className="text-center p-4"
style={{ background: "#e6f7ff", borderRadius: 8 }}
>
<Settings
className="w-8 h-8 mx-auto mb-2"
style={{ color: "#1890ff" }}
/>
<div
className="text-2xl font-bold"
style={{ color: "#1890ff" }}
>
{selectedTemplate.nodes.length}
</div>
<div className="text-sm" style={{ color: "#888" }}>
</div>
</div>
<div
className="text-center p-4"
style={{ background: "#f6ffed", borderRadius: 8 }}
>
<ArrowRight
className="w-8 h-8 mx-auto mb-2"
style={{ color: "#52c41a" }}
/>
<div
className="text-2xl font-bold"
style={{ color: "#52c41a" }}
>
{selectedTemplate.connections.length}
</div>
<div className="text-sm" style={{ color: "#888" }}>
</div>
</div>
<div
className="text-center p-4"
style={{ background: "#f9f0ff", borderRadius: 8 }}
>
<Target
className="w-8 h-8 mx-auto mb-2"
style={{ color: "#722ed1" }}
/>
<div
className="text-2xl font-bold"
style={{ color: "#722ed1" }}
>
{selectedTemplate.usageCount}
</div>
<div className="text-sm" style={{ color: "#888" }}>
使
</div>
</div>
<div
className="text-center p-4"
style={{ background: "#fff7e6", borderRadius: 8 }}
>
<Clock
className="w-8 h-8 mx-auto mb-2"
style={{ color: "#fa8c16" }}
/>
<div
className="text-2xl font-bold"
style={{ color: "#fa8c16" }}
>
{selectedTemplate.createdAt}
</div>
<div className="text-sm" style={{ color: "#888" }}>
</div>
</div>
</div>
<div style={{ marginTop: 32 }}>
<h4 style={{ fontWeight: 600, marginBottom: 16 }}>
</h4>
<div className="space-y-3">
{selectedTemplate.nodes.map((node, index) => (
<div
key={node.id}
className="flex items-start gap-3 p-4"
style={{
border: "1px solid #f0f0f0",
borderRadius: 8,
marginBottom: 12,
}}
>
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center text-sm font-medium">
{index + 1}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getNodeIcon(node.type)}
<span style={{ fontWeight: 500 }}>{node.name}</span>
<Badge color="blue">
{
nodeTypes.find((nt) => nt.type === node.type)
?.category
}
</Badge>
</div>
<div style={{ color: "#888", marginBottom: 8 }}>
{node.description}
</div>
{Object.keys(node.config).length > 0 && (
<div
style={{
fontSize: 12,
color: "#888",
background: "#fafafa",
borderRadius: 4,
padding: 8,
}}
>
<strong>:</strong>{" "}
{JSON.stringify(node.config, null, 2)}
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,462 @@
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<string | null>(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 (
<div className="h-screen flex bg-gray-50">
{/* Header */}
<div className="absolute top-0 left-0 right-0 z-50 bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
type="text"
size="small"
onClick={onBack}
className="text-gray-600 hover:text-gray-900"
icon={<ArrowLeft className="w-4 h-4 mr-2" />}
>
</Button>
<div className="h-6 w-px bg-gray-300" />
<div>
<Input
value={workflow.name}
onChange={(e) =>
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}
/>
<Input
value={workflow.description}
onChange={(e) =>
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}
/>
</div>
</div>
<div className="flex items-center gap-3">
<Button
type="default"
size="small"
icon={<Bug className="w-4 h-4 mr-2" />}
>
</Button>
<Button
type="default"
size="small"
icon={<Play className="w-4 h-4 mr-2" />}
>
</Button>
<Button
type="primary"
onClick={handleSave}
size="small"
icon={<Save className="w-4 h-4 mr-2" />}
>
</Button>
</div>
</div>
</div>
{/* Component Library Sidebar */}
<div className="w-80 bg-white border-r border-gray-200 flex flex-col mt-20">
<div className="p-4 border-b border-gray-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索组件..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div style={{ flex: 1, overflowY: "auto" }}>
<div className="p-4 space-y-3">
{filteredNodeTypes.map((nodeType) => (
<Card
key={nodeType.type}
className="cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"
draggable
onDragStart={(event) => onDragStart(event, nodeType.type)}
bodyStyle={{ padding: 16 }}
>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<nodeType.icon className="w-5 h-5 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900 mb-1">
{nodeType.name}
</div>
<div className="text-sm text-gray-600 leading-relaxed">
{nodeType.description}
</div>
<Badge color="blue" style={{ marginTop: 8, fontSize: 12 }}>
{nodeType.category}
</Badge>
</div>
</div>
</Card>
))}
</div>
</div>
</div>
{/* Main Canvas */}
<div className="flex-1 mt-20">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onDrop={onDrop}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
fitView
className="bg-gray-50"
connectionLineStyle={{
stroke: "#3b82f6",
strokeWidth: 3,
strokeDasharray: "5,5",
}}
defaultEdgeOptions={{
type: "smoothstep",
animated: true,
style: {
stroke: "#3b82f6",
strokeWidth: 3,
strokeDasharray: "0",
},
markerEnd: {
type: "arrowclosed",
color: "#3b82f6",
},
}}
isValidConnection={(connection) =>
connection.source !== connection.target
}
>
<Controls />
<MiniMap />
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
</ReactFlow>
</div>
{/* Properties Panel */}
{selectedNodeId && (
<div className="w-80 bg-white border-l border-gray-200 mt-20">
<div className="p-4 border-b border-gray-200">
<Title level={4} style={{ margin: 0 }}>
</Title>
</div>
<div style={{ height: "calc(100% - 56px)", overflowY: "auto" }}>
<div className="p-4 ">
{(() => {
const selectedNode = nodes.find(
(node) => node.id === selectedNodeId
);
if (!selectedNode) return null;
return (
<>
<div>
<label
htmlFor="node-name"
className="block font-medium mb-1"
>
</label>
<Input
id="node-name"
value={selectedNode.data.name}
onChange={(e) => {
setNodes((nds) =>
nds.map((node) =>
node.id === selectedNode.id
? {
...node,
data: {
...node.data,
name: e.target.value,
},
}
: node
)
);
}}
className="mt-1"
/>
</div>
<div>
<label
htmlFor="node-description"
className="block font-medium mb-1"
>
</label>
<TextArea
id="node-description"
value={selectedNode.data.description}
onChange={(e) => {
setNodes((nds) =>
nds.map((node) =>
node.id === selectedNode.id
? {
...node,
data: {
...node.data,
description: e.target.value,
},
}
: node
)
);
}}
className="mt-1"
rows={3}
/>
</div>
</>
);
})()}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,159 @@
import { Handle, Position } from "@xyflow/react";
import { Button, Card } from "antd";
import {
Settings,
Database,
Trash2,
Copy,
ChevronDown,
MessageSquare,
Brain,
Cpu,
} from "lucide-react";
import { useState } from "react";
const CustomNode = ({ data, selected }: { data: any; selected: boolean }) => {
const [isHovered, setIsHovered] = useState(false);
const getNodeIcon = (type: string) => {
switch (type) {
case "knowledge-search":
return <Database className="w-4 h-4 text-blue-600" />;
case "ai-dialogue":
return <MessageSquare className="w-4 h-4 text-blue-600" />;
case "data-processing":
return <Cpu className="w-4 h-4 text-blue-600" />;
default:
return <Brain className="w-4 h-4 text-blue-600" />;
}
};
return (
<div
className="relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Left side handles - inputs */}
<Handle
type="target"
position={Position.Left}
id="left-input"
className="w-3 h-3 bg-green-500 border-2 border-white shadow-md hover:bg-green-600 transition-all duration-200 hover:scale-110"
style={{ left: -6, top: "50%" }}
/>
{/* Right side handles - outputs */}
<Handle
type="source"
position={Position.Right}
id="right-output"
className="w-3 h-3 bg-blue-500 border-2 border-white shadow-md hover:bg-blue-600 transition-all duration-200 hover:scale-110"
style={{ right: -6, top: "50%" }}
/>
<Card
className={`w-80 transition-all duration-200 ${
selected
? "ring-2 ring-blue-500 shadow-lg"
: "shadow-md hover:shadow-lg"
}`}
bodyStyle={{ padding: 0 }}
>
<div className="pb-3 bg-blue-50 border-b px-4 pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
{getNodeIcon(data.type)}
</div>
<div>
<div className="font-semibold text-gray-900">{data.name}</div>
<div className="text-sm text-gray-600">{data.description}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="text"
size="small"
onClick={(e) => {
e.stopPropagation();
data.onDuplicate?.(data.id);
}}
className="h-8 w-8 p-0 text-gray-500 hover:text-gray-700"
icon={<Copy className="w-4 h-4" />}
/>
<Button
type="text"
size="small"
onClick={(e) => {
e.stopPropagation();
data.onDelete?.(data.id);
}}
className="h-8 w-8 p-0 text-gray-500 hover:text-red-600"
icon={<Trash2 className="w-4 h-4" />}
/>
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Input Section */}
<div>
<div className="font-medium text-gray-900 mb-3 flex items-center gap-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">AI </span>
<div className="flex items-center gap-2">
<span className="text-sm font-medium"></span>
<ChevronDown className="w-4 h-4 text-gray-400" />
</div>
</div>
<Button type="primary" className="w-full">
<Settings className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* Parameters Table */}
<div>
<div className="font-medium text-gray-900 mb-3"></div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="grid grid-cols-5 gap-2 text-xs font-medium text-gray-600 mb-2">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div className="grid grid-cols-5 gap-2 text-xs text-gray-700">
<div></div>
<div>5000</div>
<div>0.4</div>
<div className="text-red-500"></div>
<div>Qwen-max</div>
</div>
</div>
</div>
{/* Output Section */}
<div>
<div className="font-medium text-gray-900 mb-2 flex items-center gap-2">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600"></span>
<span className="text-gray-500"></span>
</div>
</div>
</div>
</Card>
</div>
);
};
export default CustomNode;