phase1-agent-canvas-done
CI / gate (push) Has been cancelled

This commit is contained in:
QihuAI Design committed 2026-07-27 23:20:22 +08:00
1 parent 96be3fdeb3
commit 1633d5c33e
3 files changed
+245 -1

No files matched your search

+3 -1
View File
@@ -14,6 +14,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"dagre": "^0.8.5",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
@@ -22,7 +23,8 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.30.3" "react-router-dom": "^6.30.3",
"reactflow": "^11.11.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.29", "@types/react": "^18.3.29",
+66
View File
@@ -0,0 +1,66 @@
.agent-canvas-page {
display: flex;
flex-direction: column;
height: calc(100vh - 60px);
background: #0f1117;
}
.canvas-toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: #1a1d27;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
}
.agent-name-input {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: #e0e0e0;
padding: 6px 12px;
font-size: 14px;
width: 200px;
}
.toolbar-actions { display: flex; gap: 8px; margin-left: auto; }
.tb-btn {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.05);
color: #ccc;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tb-btn:hover { background: rgba(255,255,255,0.1); }
.tb-btn.primary { background: #5B5FED; border-color: #5B5FED; color: #fff; }
.tb-btn.primary:hover { background: #4A4ECC; }
.tb-btn.secondary { background: rgba(255,255,255,0.03); }
.canvas-container { flex: 1; min-height: 0; }
.agent-node {
padding: 10px 14px;
border-radius: 10px;
min-width: 140px;
font-size: 12px;
border: 2px solid rgba(255,255,255,0.1);
background: #1e2130;
}
.agent-node .node-icon {
width: 28px; height: 28px;
border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px; margin-bottom: 4px;
}
.skill-node .node-icon { background: rgba(91,95,237,0.2); color: #5B5FED; }
.llm-node .node-icon { background: rgba(46,204,113,0.2); color: #2ecc71; }
.mcp-node .node-icon { background: rgba(241,196,15,0.2); color: #f1c40f; }
.io-node .node-icon { background: rgba(52,152,219,0.2); color: #3498db; }
.io-node.output .node-icon { background: rgba(231,76,60,0.2); color: #e74c3c; }
.node-label { font-weight: 600; color: #e0e0e0; margin: 2px 0; font-size: 13px; }
.node-meta { color: #888; font-size: 11px; }
.canvas-hint { color: #555; font-size: 12px; }
.react-flow__controls button { background: #1a1d27; border-color: rgba(255,255,255,0.1); color: #ccc; }
.react-flow__minimap { background: #1a1d27; }
+176
View File
@@ -0,0 +1,176 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Node, Edge, Controls, Background, MiniMap,
useNodesState, useEdgesState, addEdge, Connection,
MarkerType, Panel, NodeTypes,
} from 'reactflow';
import 'reactflow/dist/style.css';
import dagre from 'dagre';
import './AgentCanvas.css';
const nodeTypes: NodeTypes = {
skillNode: SkillNode,
llmNode: LLMNode,
mcpNode: MCPNode,
ioNode: IONode,
};
function SkillNode({ data }: any) {
return (
<div className="agent-node skill-node">
<div className="node-icon">S</div>
<div className="node-label">{data.label}</div>
<div className="node-meta">{data.desc}</div>
</div>
);
}
function LLMNode({ data }: any) {
return (
<div className="agent-node llm-node">
<div className="node-icon">AI</div>
<div className="node-label">{data.label}</div>
<div className="node-meta">{data.model || 'deepseek-v4-flash'}</div>
</div>
);
}
function MCPNode({ data }: any) {
return (
<div className="agent-node mcp-node">
<div className="node-icon">M</div>
<div className="node-label">{data.label}</div>
<div className="node-meta">{data.server}</div>
</div>
);
}
function IONode({ data }: any) {
return (
<div className={`agent-node io-node ${data.ioType}`}>
<div className="node-icon">{data.ioType === 'input' ? 'IN' : 'OUT'}</div>
<div className="node-label">{data.label}</div>
</div>
);
}
const defaultNodes: Node[] = [
{ id: 'input-1', type: 'ioNode', position: { x: 100, y: 250 }, data: { label: '用户输入', ioType: 'input' } },
{ id: 'llm-1', type: 'llmNode', position: { x: 350, y: 200 }, data: { label: '推理节点', model: 'deepseek-v4-flash' } },
{ id: 'skill-1', type: 'skillNode', position: { x: 350, y: 400 }, data: { label: 'PPT生成', desc: '文本→PPTX' } },
{ id: 'output-1', type: 'ioNode', position: { x: 600, y: 250 }, data: { label: '输出结果', ioType: 'output' } },
];
const defaultEdges: Edge[] = [
{ id: 'e1-2', source: 'input-1', target: 'llm-1', animated: true, markerEnd: { type: MarkerType.ArrowClosed } },
{ id: 'e1-3', source: 'input-1', target: 'skill-1', animated: true, markerEnd: { type: MarkerType.ArrowClosed } },
{ id: 'e2-4', source: 'llm-1', target: 'output-1', markerEnd: { type: MarkerType.ArrowClosed } },
{ id: 'e3-4', source: 'skill-1', target: 'output-1', markerEnd: { type: MarkerType.ArrowClosed } },
];
function layoutDagre(nodes: Node[], edges: Edge[]) {
const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'LR', nodesep: 80, ranksep: 120 });
nodes.forEach(n => g.setNode(n.id, { width: 160, height: 80 }));
edges.forEach(e => g.setEdge(e.source, e.target));
dagre.layout(g);
return nodes.map(n => {
const pos = g.node(n.id);
return { ...n, position: { x: pos.x - 80, y: pos.y - 40 } };
});
}
export default function AgentCanvas() {
const [nodes, setNodes, onNodesChange] = useNodesState(layoutDagre(defaultNodes, defaultEdges));
const [edges, setEdges, onEdgesChange] = useEdgesState(defaultEdges);
const [agentName, setAgentName] = useState('新智能体');
const [saved, setSaved] = useState(false);
const onConnect = useCallback((params: Connection) =>
setEdges(eds => addEdge({ ...params, animated: true, markerEnd: { type: MarkerType.ArrowClosed } }, eds)),
[setEdges]
);
const addNode = useCallback((type: string) => {
const id = `${type}-${Date.now()}`;
const labels: Record<string, any> = {
skillNode: { label: '新技能', desc: '技能描述' },
llmNode: { label: 'LLM节点', model: 'deepseek-v4-flash' },
mcpNode: { label: 'MCP工具', server: 'tool-server' },
};
const newNode: Node = {
id, type, position: { x: 300, y: 100 + nodes.length * 80 },
data: labels[type] || { label: id },
};
setNodes(nds => [...nds, newNode]);
}, [nodes.length, setNodes]);
const handleAutoLayout = useCallback(() => {
setNodes(nds => layoutDagre(nds, edges));
}, [edges, setNodes]);
const handleSave = useCallback(async () => {
const config = { name: agentName, nodes, edges, updatedAt: new Date().toISOString() };
try {
const token = localStorage.getItem('token');
await fetch('/app/api/agent/dags', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(config),
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (e) {
console.error('Save failed', e);
}
}, [agentName, nodes, edges]);
const handleClear = useCallback(() => {
setNodes([]);
setEdges([]);
}, [setNodes, setEdges]);
return (
<div className="agent-canvas-page">
<div className="canvas-toolbar">
<input
className="agent-name-input"
value={agentName}
onChange={e => setAgentName(e.target.value)}
placeholder="智能体名称"
/>
<div className="toolbar-actions">
<button className="tb-btn" onClick={() => addNode('skillNode')}>+ 技能</button>
<button className="tb-btn" onClick={() => addNode('llmNode')}>+ LLM</button>
<button className="tb-btn" onClick={() => addNode('mcpNode')}>+ MCP</button>
<button className="tb-btn secondary" onClick={handleAutoLayout}>自动布局</button>
<button className="tb-btn secondary" onClick={handleClear}>清空</button>
<button className="tb-btn primary" onClick={handleSave}>
{saved ? '已保存 ✓' : '保存配置'}
</button>
</div>
</div>
<div className="canvas-container">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
snapToGrid
snapGrid={[20, 20]}
>
<Controls />
<Background gap={20} size={1} />
<MiniMap nodeStrokeWidth={3} pannable zoomable />
<Panel position="bottom-center">
<span className="canvas-hint">拖拽节点 · 连线编排 · 双击编辑标签</span>
</Panel>
</ReactFlow>
</div>
</div>
);
}