diff --git a/dashboard/package.json b/dashboard/package.json index 59d3f3042..cd621b0b7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -20,16 +20,14 @@ "axios": "^1.6.2", "axios-mock-adapter": "^1.22.0", "chance": "1.1.11", + "d3": "^7.9.0", "date-fns": "2.30.0", - "graphology": "^0.26.0", - "graphology-layout-force": "^0.2.4", "highlight.js": "^11.11.1", "js-md5": "^0.8.3", "lodash": "4.17.21", "marked": "^15.0.7", "pinia": "2.1.6", "remixicon": "3.5.0", - "sigma": "^3.0.1", "vee-validate": "4.11.3", "vite-plugin-vuetify": "1.0.2", "vue": "3.3.4", diff --git a/dashboard/src/views/AlkaidPage.vue b/dashboard/src/views/AlkaidPage.vue index bae914445..80d4b3c3c 100644 --- a/dashboard/src/views/AlkaidPage.vue +++ b/dashboard/src/views/AlkaidPage.vue @@ -1,7 +1,6 @@ @@ -142,12 +141,14 @@ export default { }, data() { return { - renderer: null, - graph: null, - layout: null, + simulation: null, + svg: null, + zoom: null, activeTab: 'long-term-memory', node_data: [], edge_data: [], + nodes: [], + links: [], searchUserId: null, userIdList: [], selectedNode: null, @@ -167,34 +168,31 @@ export default { } }, mounted() { - this.initSigma(); + this.initD3Graph(); this.ltmGetGraph(); this.ltmGetUserIds(); }, beforeUnmount() { - if (this.renderer) { - this.renderer.kill(); - } - if (this.layout) { - this.layout.stop(); + if (this.simulation) { + this.simulation.stop(); } }, watch: { activeTab(newVal) { if (newVal === 'long-term-memory') { this.$nextTick(() => { - if (!this.renderer) { - this.initSigma(); + if (!this.svg) { + this.initD3Graph(); } }); } else { - if (this.renderer) { - this.renderer.kill(); - this.renderer = null; + if (this.simulation) { + this.simulation.stop(); + this.simulation = null; } - if (this.layout) { - this.layout.stop(); - this.layout = null; + if (this.svg) { + d3.select("#graph-container svg").remove(); + this.svg = null; } } } @@ -206,62 +204,46 @@ export default { axios.get('/api/plug/alkaid/ltm/graph', { params }) .then(response => { - let nodes = response.data.data.nodes; - let edges = response.data.data.edges; + let nodesRaw = response.data.data.nodes; + let edgesRaw = response.data.data.edges; - this.node_data = nodes; - this.edge_data = edges; + this.node_data = nodesRaw; + this.edge_data = edgesRaw; - if (this.graph) { - this.graph.clear(); - } - - - - nodes.forEach(node => { + // 转换为D3所需的数据格式 + this.nodes = nodesRaw.map(node => { const nodeId = node[0]; const nodeData = node[1]; - - if (!this.graph.hasNode(nodeId)) { - const nodeType = nodeData._label || 'default'; - const color = this.nodeColors[nodeType] || this.nodeColors['default']; - - this.graph.addNode(nodeId, { - x: Math.random(), - y: Math.random(), - size: 5, - label: nodeData.name || nodeId.split('_')[0], - color: color, - originalData: nodeData - }); - } + const nodeType = nodeData._label || 'default'; + const color = this.nodeColors[nodeType] || this.nodeColors['default']; + + return { + id: nodeId, + label: nodeData.name || nodeId.split('_')[0], + color: color, + originalData: nodeData + }; }); - // 添加边 - edges.forEach(edge => { + this.links = edgesRaw.map(edge => { const sourceId = edge[0]; const targetId = edge[1]; const edgeData = edge[2]; - - if (this.graph.hasNode(sourceId) && this.graph.hasNode(targetId)) { - const edgeId = `${sourceId}->${targetId}`; - const relationType = edgeData.relation_type || 'default'; - const color = this.edgeColors[relationType] || this.edgeColors['default']; - this.graph.addEdge(sourceId, targetId, { - size: 1, - color: color, - originalData: edgeData, - label: relationType, - type: "line" - }); - } else { - console.warn(`Edge ${sourceId} -> ${targetId} has missing nodes.`); - } + const relationType = edgeData.relation_type || 'default'; + const color = this.edgeColors[relationType] || this.edgeColors['default']; + + return { + source: sourceId, + target: targetId, + color: color, + originalData: edgeData, + label: relationType + }; }); + this.updateD3Graph(); this.updateGraphStats(); - - console.log('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges'); + console.log('Graph initialized with', this.nodes.length, 'nodes and', this.links.length, 'links'); }) .catch(error => { console.error('Error fetching graph data:', error); @@ -269,11 +251,6 @@ export default { .finally(() => { this.isLoading = false; }); - - if (this.layout) { - this.layout.start(); - } - }, ltmGetUserIds() { @@ -287,12 +264,10 @@ export default { }, updateGraphStats() { - if (this.graph) { - this.graphStats = { - nodeCount: this.graph.order, - edgeCount: this.graph.size - }; - } + this.graphStats = { + nodeCount: this.nodes.length, + edgeCount: this.links.length + }; }, refreshGraph() { @@ -301,7 +276,7 @@ export default { onNodeSelect() { console.log('Selected user ID:', this.searchUserId); - if (!this.searchUserId || !this.graph) return; + if (!this.searchUserId) return; // 使用API的user_id参数筛选数据 this.ltmGetGraph(this.searchUserId); @@ -312,83 +287,136 @@ export default { this.ltmGetGraph(); }, - initSigma() { + initD3Graph() { const container = document.getElementById("graph-container"); if (!container) return; + d3.select("#graph-container svg").remove(); + const width = container.clientWidth; + const height = container.clientHeight; + const svg = d3.select("#graph-container") + .append("svg") + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", [0, 0, width, height]) + .classed("d3-graph", true); + const g = svg.append("g"); + const zoom = d3.zoom() + .scaleExtent([0.1, 10]) + .on("zoom", (event) => { + g.attr("transform", event.transform); + }); - if (this.renderer) { - this.renderer.kill(); - this.renderer = null; - } - if (this.layout) { - this.layout.stop(); - this.layout = null; - } + svg.call(zoom); + const simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(d => d.id).distance(100)) + .force("charge", d3.forceManyBody().strength(-300)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide().radius(30)); - const graph = new Graph({ - multi: true, + this.svg = svg; + this.g = g; + this.zoom = zoom; + this.simulation = simulation; + this.width = width; + this.height = height; + }, + + updateD3Graph() { + if (!this.svg || !this.simulation) return; + const g = this.g; + g.selectAll("*").remove(); + g.append("defs").append("marker") + .attr("id", "arrowhead") + .attr("viewBox", "0 -5 10 10") + .attr("refX", 20) + .attr("refY", 0) + .attr("orient", "auto") + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .append("path") + .attr("d", "M0,-5L10,0L0,5") + .attr("fill", "#999"); + const link = g.append("g") + .selectAll("line") + .data(this.links) + .join("line") + .attr("stroke", d => d.color) + .attr("stroke-width", 1.5) + .attr("marker-end", "url(#arrowhead)"); + const edgeLabels = g.append("g") + .selectAll("text") + .data(this.links) + .join("text") + .text(d => d.label) + .attr("font-size", "8px") + .attr("text-anchor", "middle") + .attr("fill", "#666") + .attr("dy", -5); + const node = g.append("g") + .selectAll("circle") + .data(this.nodes) + .join("circle") + .attr("r", 8) + .attr("fill", d => d.color) + .style("cursor", "pointer") + .call(this.dragBehavior()); + const nodeLabels = g.append("g") + .selectAll("text") + .data(this.nodes) + .join("text") + .text(d => d.label) + .attr("font-size", "10px") + .attr("text-anchor", "middle") + .attr("fill", "#333") + .attr("dy", -12); + node.on("click", (event, d) => { + event.stopPropagation(); + this.selectedNode = d.originalData; }); - - const layout = new ForceSupervisor(graph, { - isNodeFixed: (_, attr) => attr.highlighted, settings: { - barnesHutOptimize: true, - } - }); - layout.start(); - - this.layout = layout; - this.graph = graph; - const renderer = new Sigma(graph, container, { - minCameraRatio: 0.01, - maxCameraRatio: 2, - labelRenderedSizeThreshold: 1, - renderLabels: true, - renderEdgeLabels: true, - labelSize: 14, - labelColor: "#333333", - }); - this.renderer = renderer; - - let draggedNode = null; - let isDragging = false; - - renderer.on("downNode", (e) => { - isDragging = true; - draggedNode = e.node; - graph.setNodeAttribute(draggedNode, "highlighted", true); - if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox()); - }); - - renderer.on("moveBody", ({ event }) => { - if (!isDragging || !draggedNode) return; - const pos = renderer.viewportToGraph(event); - - graph.setNodeAttribute(draggedNode, "x", pos.x); - graph.setNodeAttribute(draggedNode, "y", pos.y); - event.preventSigmaDefault(); - event.original.preventDefault(); - event.original.stopPropagation(); - }); - const handleUp = () => { - if (draggedNode) { - graph.removeNodeAttribute(draggedNode, "highlighted"); - } - isDragging = false; - draggedNode = null; - }; - renderer.on("upNode", handleUp); - renderer.on("upStage", handleUp); - - renderer.on("clickNode", (e) => { - const nodeId = e.node; - const nodeAttributes = graph.getNodeAttributes(nodeId); - this.selectedNode = nodeAttributes.originalData; - }); - - renderer.on("clickStage", () => { + this.svg.on("click", () => { this.selectedNode = null; }); + this.simulation + .nodes(this.nodes) + .on("tick", () => { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + edgeLabels + .attr("x", d => (d.source.x + d.target.x) / 2) + .attr("y", d => (d.source.y + d.target.y) / 2); + node + .attr("cx", d => d.x) + .attr("cy", d => d.y); + nodeLabels + .attr("x", d => d.x) + .attr("y", d => d.y); + }); + this.simulation.force("link") + .links(this.links); + + this.simulation.alpha(1).restart(); + }, + + dragBehavior() { + return d3.drag() + .on("start", (event, d) => { + if (!event.active) this.simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; + d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) this.simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }); }, getRandomColor() { @@ -428,4 +456,13 @@ export default { .memory-header { padding: 0 8px; } + +#graph-container svg { + width: 100%; + height: 100%; +} + +.d3-graph { + background-color: #f2f6f9; +} \ No newline at end of file diff --git a/dashboard/src/views/AlkaidPage_d3.vue b/dashboard/src/views/AlkaidPage_sigma.vue similarity index 57% rename from dashboard/src/views/AlkaidPage_d3.vue rename to dashboard/src/views/AlkaidPage_sigma.vue index 759083db6..1b5180955 100644 --- a/dashboard/src/views/AlkaidPage_d3.vue +++ b/dashboard/src/views/AlkaidPage_sigma.vue @@ -1,6 +1,7 @@ @@ -141,14 +142,12 @@ export default { }, data() { return { - simulation: null, - svg: null, - zoom: null, + renderer: null, + graph: null, + layout: null, activeTab: 'long-term-memory', node_data: [], edge_data: [], - nodes: [], - links: [], searchUserId: null, userIdList: [], selectedNode: null, @@ -168,31 +167,34 @@ export default { } }, mounted() { - this.initD3Graph(); + this.initSigma(); this.ltmGetGraph(); this.ltmGetUserIds(); }, beforeUnmount() { - if (this.simulation) { - this.simulation.stop(); + if (this.renderer) { + this.renderer.kill(); + } + if (this.layout) { + this.layout.stop(); } }, watch: { activeTab(newVal) { if (newVal === 'long-term-memory') { this.$nextTick(() => { - if (!this.svg) { - this.initD3Graph(); + if (!this.renderer) { + this.initSigma(); } }); } else { - if (this.simulation) { - this.simulation.stop(); - this.simulation = null; + if (this.renderer) { + this.renderer.kill(); + this.renderer = null; } - if (this.svg) { - d3.select("#graph-container svg").remove(); - this.svg = null; + if (this.layout) { + this.layout.stop(); + this.layout = null; } } } @@ -204,46 +206,62 @@ export default { axios.get('/api/plug/alkaid/ltm/graph', { params }) .then(response => { - let nodesRaw = response.data.data.nodes; - let edgesRaw = response.data.data.edges; + let nodes = response.data.data.nodes; + let edges = response.data.data.edges; - this.node_data = nodesRaw; - this.edge_data = edgesRaw; + this.node_data = nodes; + this.edge_data = edges; - // 转换为D3所需的数据格式 - this.nodes = nodesRaw.map(node => { + if (this.graph) { + this.graph.clear(); + } + + + + nodes.forEach(node => { const nodeId = node[0]; const nodeData = node[1]; - const nodeType = nodeData._label || 'default'; - const color = this.nodeColors[nodeType] || this.nodeColors['default']; - - return { - id: nodeId, - label: nodeData.name || nodeId.split('_')[0], - color: color, - originalData: nodeData - }; + + if (!this.graph.hasNode(nodeId)) { + const nodeType = nodeData._label || 'default'; + const color = this.nodeColors[nodeType] || this.nodeColors['default']; + + this.graph.addNode(nodeId, { + x: Math.random(), + y: Math.random(), + size: 5, + label: nodeData.name || nodeId.split('_')[0], + color: color, + originalData: nodeData + }); + } }); - this.links = edgesRaw.map(edge => { + // 添加边 + edges.forEach(edge => { const sourceId = edge[0]; const targetId = edge[1]; const edgeData = edge[2]; - const relationType = edgeData.relation_type || 'default'; - const color = this.edgeColors[relationType] || this.edgeColors['default']; - - return { - source: sourceId, - target: targetId, - color: color, - originalData: edgeData, - label: relationType - }; + + if (this.graph.hasNode(sourceId) && this.graph.hasNode(targetId)) { + const edgeId = `${sourceId}->${targetId}`; + const relationType = edgeData.relation_type || 'default'; + const color = this.edgeColors[relationType] || this.edgeColors['default']; + this.graph.addEdge(sourceId, targetId, { + size: 1, + color: color, + originalData: edgeData, + label: relationType, + type: "line" + }); + } else { + console.warn(`Edge ${sourceId} -> ${targetId} has missing nodes.`); + } }); - this.updateD3Graph(); this.updateGraphStats(); - console.log('Graph initialized with', this.nodes.length, 'nodes and', this.links.length, 'links'); + + console.log('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges'); }) .catch(error => { console.error('Error fetching graph data:', error); @@ -251,6 +269,11 @@ export default { .finally(() => { this.isLoading = false; }); + + if (this.layout) { + this.layout.start(); + } + }, ltmGetUserIds() { @@ -264,10 +287,12 @@ export default { }, updateGraphStats() { - this.graphStats = { - nodeCount: this.nodes.length, - edgeCount: this.links.length - }; + if (this.graph) { + this.graphStats = { + nodeCount: this.graph.order, + edgeCount: this.graph.size + }; + } }, refreshGraph() { @@ -276,7 +301,7 @@ export default { onNodeSelect() { console.log('Selected user ID:', this.searchUserId); - if (!this.searchUserId) return; + if (!this.searchUserId || !this.graph) return; // 使用API的user_id参数筛选数据 this.ltmGetGraph(this.searchUserId); @@ -287,175 +312,84 @@ export default { this.ltmGetGraph(); }, - initD3Graph() { + initSigma() { const container = document.getElementById("graph-container"); if (!container) return; - // 清除旧的SVG元素 - d3.select("#graph-container svg").remove(); + if (this.renderer) { + this.renderer.kill(); + this.renderer = null; + } + if (this.layout) { + this.layout.stop(); + this.layout = null; + } - // 获取容器尺寸 - const width = container.clientWidth; - const height = container.clientHeight; - - // 创建SVG元素 - const svg = d3.select("#graph-container") - .append("svg") - .attr("width", "100%") - .attr("height", "100%") - .attr("viewBox", [0, 0, width, height]) - .classed("d3-graph", true); - - // 创建图形元素的容器 - const g = svg.append("g"); - - // 添加缩放功能 - const zoom = d3.zoom() - .scaleExtent([0.1, 10]) - .on("zoom", (event) => { - g.attr("transform", event.transform); - }); - - svg.call(zoom); - - // 初始力导向模拟 - const simulation = d3.forceSimulation() - .force("link", d3.forceLink().id(d => d.id).distance(100)) - .force("charge", d3.forceManyBody().strength(-300)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide().radius(30)); - - this.svg = svg; - this.g = g; - this.zoom = zoom; - this.simulation = simulation; - this.width = width; - this.height = height; - }, - - updateD3Graph() { - if (!this.svg || !this.simulation) return; - - const g = this.g; - - // 清除先前的元素 - g.selectAll("*").remove(); - - // 创建箭头标记 - g.append("defs").append("marker") - .attr("id", "arrowhead") - .attr("viewBox", "0 -5 10 10") - .attr("refX", 20) - .attr("refY", 0) - .attr("orient", "auto") - .attr("markerWidth", 6) - .attr("markerHeight", 6) - .append("path") - .attr("d", "M0,-5L10,0L0,5") - .attr("fill", "#999"); - - // 创建边 - const link = g.append("g") - .selectAll("line") - .data(this.links) - .join("line") - .attr("stroke", d => d.color) - .attr("stroke-width", 1.5) - .attr("marker-end", "url(#arrowhead)"); - - // 创建边上的文本标签 - const edgeLabels = g.append("g") - .selectAll("text") - .data(this.links) - .join("text") - .text(d => d.label) - .attr("font-size", "8px") - .attr("text-anchor", "middle") - .attr("fill", "#666") - .attr("dy", -5); - - // 创建节点 - const node = g.append("g") - .selectAll("circle") - .data(this.nodes) - .join("circle") - .attr("r", 8) - .attr("fill", d => d.color) - .style("cursor", "pointer") - .call(this.dragBehavior()); - - // 创建节点标签 - const nodeLabels = g.append("g") - .selectAll("text") - .data(this.nodes) - .join("text") - .text(d => d.label) - .attr("font-size", "10px") - .attr("text-anchor", "middle") - .attr("fill", "#333") - .attr("dy", -12); - - // 定义拖拽结束事件 - node.on("click", (event, d) => { - event.stopPropagation(); - this.selectedNode = d.originalData; + const graph = new Graph({ + multi: true, }); - // 画布点击事件,清除选中节点 - this.svg.on("click", () => { + const layout = new ForceSupervisor(graph, { + isNodeFixed: (_, attr) => attr.highlighted, settings: { + gravity: 0.0001, + repulsion: 0.001 + } + }); + layout.start(); + + this.layout = layout; + this.graph = graph; + const renderer = new Sigma(graph, container, { + minCameraRatio: 0.01, + maxCameraRatio: 2, + labelRenderedSizeThreshold: 1, + renderLabels: true, + renderEdgeLabels: true, + labelSize: 14, + labelColor: "#333333", + }); + this.renderer = renderer; + + let draggedNode = null; + let isDragging = false; + + renderer.on("downNode", (e) => { + isDragging = true; + draggedNode = e.node; + graph.setNodeAttribute(draggedNode, "highlighted", true); + if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox()); + }); + + renderer.on("moveBody", ({ event }) => { + if (!isDragging || !draggedNode) return; + const pos = renderer.viewportToGraph(event); + + graph.setNodeAttribute(draggedNode, "x", pos.x); + graph.setNodeAttribute(draggedNode, "y", pos.y); + event.preventSigmaDefault(); + event.original.preventDefault(); + event.original.stopPropagation(); + }); + const handleUp = () => { + if (draggedNode) { + graph.removeNodeAttribute(draggedNode, "highlighted"); + } + isDragging = false; + draggedNode = null; + }; + renderer.on("upNode", handleUp); + renderer.on("upStage", handleUp); + + renderer.on("clickNode", (e) => { + const nodeId = e.node; + const nodeAttributes = graph.getNodeAttributes(nodeId); + this.selectedNode = nodeAttributes.originalData; + }); + + renderer.on("clickStage", () => { this.selectedNode = null; }); - // 更新力导向模拟 - this.simulation - .nodes(this.nodes) - .on("tick", () => { - // 更新链接位置 - link - .attr("x1", d => d.source.x) - .attr("y1", d => d.source.y) - .attr("x2", d => d.target.x) - .attr("y2", d => d.target.y); - - // 更新边标签位置 - edgeLabels - .attr("x", d => (d.source.x + d.target.x) / 2) - .attr("y", d => (d.source.y + d.target.y) / 2); - - // 更新节点位置 - node - .attr("cx", d => d.x) - .attr("cy", d => d.y); - - // 更新节点标签位置 - nodeLabels - .attr("x", d => d.x) - .attr("y", d => d.y); - }); - - this.simulation.force("link") - .links(this.links); - - // 重启模拟 - this.simulation.alpha(1).restart(); - }, - - dragBehavior() { - return d3.drag() - .on("start", (event, d) => { - if (!event.active) this.simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - }) - .on("drag", (event, d) => { - d.fx = event.x; - d.fy = event.y; - }) - .on("end", (event, d) => { - if (!event.active) this.simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - }); }, getRandomColor() { @@ -495,13 +429,4 @@ export default { .memory-header { padding: 0 8px; } - -#graph-container svg { - width: 100%; - height: 100%; -} - -.d3-graph { - background-color: #f2f6f9; -} \ No newline at end of file