/**
* KnowledgeGraph Component
* D3.js-based force-directed graph visualization for agent knowledge.
*/
import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import {
Box,
Typography,
Card,
CardContent,
CircularProgress,
Alert,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import {
ZoomIn as ZoomInIcon,
ZoomOut as ZoomOutIcon,
CenterFocusStrong as CenterIcon,
} from '@mui/icons-material';
import { getAgentGraph } from '../api/client';
const KnowledgeGraph = ({ agentName, width = 800, height = 600 }) => {
const svgRef = useRef(null);
const containerRef = useRef(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [graphData, setGraphData] = useState(null);
const [selectedNode, setSelectedNode] = useState(null);
// Color scale for node types
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
useEffect(() => {
loadGraphData();
}, [agentName]);
useEffect(() => {
if (graphData && svgRef.current) {
renderGraph();
}
}, [graphData]);
const loadGraphData = async () => {
try {
setLoading(true);
setError(null);
const data = await getAgentGraph(agentName);
setGraphData(data);
} catch (err) {
setError(err.response?.data?.detail || 'Failed to load knowledge graph');
} finally {
setLoading(false);
}
};
const renderGraph = () => {
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
const { nodes, links } = graphData;
if (!nodes || nodes.length === 0) {
return;
}
// Create zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
container.attr('transform', event.transform);
});
svg.call(zoom);
// Create container for zoom
const container = svg.append('g');
// Create arrow markers for links
svg.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', 'M 0,-5 L 10,0 L 0,5')
.attr('fill', '#666');
// Create force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links)
.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(40));
// Create links
const link = container.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', '#666')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', d => Math.sqrt(d.weight || 1))
.attr('marker-end', 'url(#arrowhead)');
// Create link labels
const linkLabel = container.append('g')
.attr('class', 'link-labels')
.selectAll('text')
.data(links)
.enter()
.append('text')
.attr('font-size', '10px')
.attr('fill', '#888')
.attr('text-anchor', 'middle')
.text(d => d.label || '');
// Create nodes
const node = container.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(nodes)
.enter()
.append('g')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
// Add circles to nodes
node.append('circle')
.attr('r', 15)
.attr('fill', d => colorScale(d.type || d.group))
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.style('cursor', 'pointer')
.on('click', (event, d) => {
setSelectedNode(d);
})
.on('mouseover', function () {
d3.select(this).attr('r', 20);
})
.on('mouseout', function () {
d3.select(this).attr('r', 15);
});
// Add labels to nodes
node.append('text')
.attr('dx', 20)
.attr('dy', 5)
.attr('font-size', '12px')
.attr('fill', '#fff')
.text(d => d.label || d.id);
// Update positions on tick
simulation.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);
linkLabel
.attr('x', d => (d.source.x + d.target.x) / 2)
.attr('y', d => (d.source.y + d.target.y) / 2);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
// Drag functions
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
// Store zoom for controls
svgRef.current.zoomBehavior = zoom;
svgRef.current.svgSelection = svg;
};
const handleZoomIn = () => {
const svg = d3.select(svgRef.current);
svg.transition().call(svgRef.current.zoomBehavior.scaleBy, 1.5);
};
const handleZoomOut = () => {
const svg = d3.select(svgRef.current);
svg.transition().call(svgRef.current.zoomBehavior.scaleBy, 0.67);
};
const handleCenter = () => {
const svg = d3.select(svgRef.current);
svg.transition().call(
svgRef.current.zoomBehavior.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(1)
);
};
if (loading) {
return (
);
}
if (error) {
return (
{error}
);
}
return (
🕸️ Knowledge Graph
{graphData?.stats && (
<>
>
)}
{selectedNode && (
Selected Node
ID: {selectedNode.id}
Label: {selectedNode.label}
Type: {selectedNode.type || 'entity'}
)}
);
};
export default KnowledgeGraph;