Spaces:
Running
Running
AgentGraph
/
frontend
/src
/components
/features
/dashboard
/visualizations
/StructuralConnectivityAnalysis.tsx
| import React, { useMemo } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { Network, BarChart3, Target, Activity } from "lucide-react"; | |
| interface ConnectedComponent { | |
| id: number; | |
| nodeIds: string[]; | |
| size: number; | |
| density: number; | |
| maxDegree: number; | |
| avgDegree: number; | |
| } | |
| interface DegreeDistribution { | |
| [degree: number]: { | |
| count: number; | |
| percentage: number; | |
| entityTypes: string[]; | |
| }; | |
| } | |
| interface StructuralPattern { | |
| type: "star" | "chain" | "cluster" | "hub" | "isolated"; | |
| description: string; | |
| nodeIds: string[]; | |
| centralNodeId?: string; | |
| score: number; | |
| } | |
| interface StructuralData { | |
| totalNodes: number; | |
| totalEdges: number; | |
| networkDensity: number; | |
| avgDegree: number; | |
| maxDegree: number; | |
| connectedComponents: ConnectedComponent[]; | |
| degreeDistribution: DegreeDistribution; | |
| structuralPatterns: StructuralPattern[]; | |
| networkHealth: number; | |
| isolatedNodes: string[]; | |
| } | |
| export function StructuralConnectivityAnalysis() { | |
| const { state } = useAgentGraph(); | |
| const structuralData = useMemo<StructuralData>(() => { | |
| const nodeMap = new Map< | |
| string, | |
| { id: string; type: string; name: string } | |
| >(); | |
| const adjacencyList = new Map<string, Set<string>>(); | |
| const edgeSet = new Set<string>(); | |
| // Process all traces and their knowledge graphs | |
| state.traces.forEach((trace) => { | |
| trace.knowledge_graphs?.forEach((kg: any) => { | |
| if (kg.graph_data) { | |
| try { | |
| const graphData = | |
| typeof kg.graph_data === "string" | |
| ? JSON.parse(kg.graph_data) | |
| : kg.graph_data; | |
| // Build node map | |
| if (graphData.entities && Array.isArray(graphData.entities)) { | |
| graphData.entities.forEach((entity: any) => { | |
| nodeMap.set(entity.id, { | |
| id: entity.id, | |
| type: entity.type || "Unknown", | |
| name: entity.name || entity.id, | |
| }); | |
| adjacencyList.set(entity.id, new Set()); | |
| }); | |
| } | |
| // Build adjacency list and count edges | |
| if (graphData.relations && Array.isArray(graphData.relations)) { | |
| graphData.relations.forEach((relation: any) => { | |
| const sourceId = relation.source; | |
| const targetId = relation.target; | |
| const edgeKey = [sourceId, targetId].sort().join("-"); | |
| if ( | |
| nodeMap.has(sourceId) && | |
| nodeMap.has(targetId) && | |
| !edgeSet.has(edgeKey) | |
| ) { | |
| adjacencyList.get(sourceId)?.add(targetId); | |
| adjacencyList.get(targetId)?.add(sourceId); | |
| edgeSet.add(edgeKey); | |
| } | |
| }); | |
| } | |
| } catch (error) { | |
| console.warn("Error parsing graph_data:", error); | |
| } | |
| } | |
| }); | |
| }); | |
| const totalNodes = nodeMap.size; | |
| const totalEdges = edgeSet.size; | |
| // Calculate basic metrics | |
| const networkDensity = | |
| totalNodes > 1 ? (2 * totalEdges) / (totalNodes * (totalNodes - 1)) : 0; | |
| const degrees = Array.from(adjacencyList.values()).map( | |
| (neighbors) => neighbors.size | |
| ); | |
| const avgDegree = | |
| degrees.length > 0 | |
| ? Math.round( | |
| (degrees.reduce((sum, d) => sum + d, 0) / degrees.length) * 10 | |
| ) / 10 | |
| : 0; | |
| const maxDegree = Math.max(...degrees, 0); | |
| // Find connected components using DFS | |
| const visited = new Set<string>(); | |
| const connectedComponents: ConnectedComponent[] = []; | |
| const dfs = (nodeId: string, component: string[]): void => { | |
| if (visited.has(nodeId)) return; | |
| visited.add(nodeId); | |
| component.push(nodeId); | |
| const neighbors = adjacencyList.get(nodeId) || new Set(); | |
| neighbors.forEach((neighborId) => { | |
| if (!visited.has(neighborId)) { | |
| dfs(neighborId, component); | |
| } | |
| }); | |
| }; | |
| let componentId = 0; | |
| nodeMap.forEach((_, nodeId) => { | |
| if (!visited.has(nodeId)) { | |
| const component: string[] = []; | |
| dfs(nodeId, component); | |
| if (component.length > 0) { | |
| const componentEdges = | |
| component.reduce((count, nodeId) => { | |
| const neighbors = adjacencyList.get(nodeId) || new Set(); | |
| const internalNeighbors = Array.from(neighbors).filter((n) => | |
| component.includes(n) | |
| ); | |
| return count + internalNeighbors.length; | |
| }, 0) / 2; // Divide by 2 since each edge is counted twice | |
| const componentDensity = | |
| component.length > 1 | |
| ? (2 * componentEdges) / | |
| (component.length * (component.length - 1)) | |
| : 0; | |
| const componentDegrees = component.map( | |
| (nodeId) => (adjacencyList.get(nodeId) || new Set()).size | |
| ); | |
| connectedComponents.push({ | |
| id: componentId++, | |
| nodeIds: component, | |
| size: component.length, | |
| density: Math.round(componentDensity * 1000) / 1000, | |
| maxDegree: Math.max(...componentDegrees, 0), | |
| avgDegree: | |
| componentDegrees.length > 0 | |
| ? Math.round( | |
| (componentDegrees.reduce((sum, d) => sum + d, 0) / | |
| componentDegrees.length) * | |
| 10 | |
| ) / 10 | |
| : 0, | |
| }); | |
| } | |
| } | |
| }); | |
| // Calculate degree distribution | |
| const degreeDistribution: DegreeDistribution = {}; | |
| Array.from(nodeMap.entries()).forEach(([nodeId, node]) => { | |
| const degree = (adjacencyList.get(nodeId) || new Set()).size; | |
| if (!degreeDistribution[degree]) { | |
| degreeDistribution[degree] = { | |
| count: 0, | |
| percentage: 0, | |
| entityTypes: [], | |
| }; | |
| } | |
| degreeDistribution[degree].count++; | |
| if (!degreeDistribution[degree].entityTypes.includes(node.type)) { | |
| degreeDistribution[degree].entityTypes.push(node.type); | |
| } | |
| }); | |
| // Calculate percentages | |
| Object.values(degreeDistribution).forEach((dist) => { | |
| dist.percentage = | |
| totalNodes > 0 ? Math.round((dist.count / totalNodes) * 100) : 0; | |
| }); | |
| // Identify structural patterns | |
| const structuralPatterns: StructuralPattern[] = []; | |
| // Find star patterns (one central node with many connections, others mostly connected to center) | |
| Array.from(nodeMap.keys()).forEach((nodeId) => { | |
| const neighbors = adjacencyList.get(nodeId) || new Set(); | |
| if (neighbors.size >= 3) { | |
| // Check if this is a potential star center | |
| const neighborDegrees = Array.from(neighbors).map( | |
| (nId) => (adjacencyList.get(nId) || new Set()).size | |
| ); | |
| const avgNeighborDegree = | |
| neighborDegrees.reduce((sum, d) => sum + d, 0) / | |
| neighborDegrees.length; | |
| if (neighbors.size >= avgNeighborDegree * 1.5) { | |
| structuralPatterns.push({ | |
| type: "star", | |
| description: `Star pattern centered on ${ | |
| nodeMap.get(nodeId)?.name | |
| }`, | |
| nodeIds: [nodeId, ...Array.from(neighbors)], | |
| centralNodeId: nodeId, | |
| score: neighbors.size, | |
| }); | |
| } | |
| } | |
| }); | |
| // Find hub patterns (nodes with significantly higher degree than average) | |
| const degreeThreshold = avgDegree * 2; | |
| Array.from(nodeMap.keys()).forEach((nodeId) => { | |
| const degree = (adjacencyList.get(nodeId) || new Set()).size; | |
| if (degree >= degreeThreshold && degree >= 4) { | |
| structuralPatterns.push({ | |
| type: "hub", | |
| description: `High-degree hub: ${nodeMap.get(nodeId)?.name}`, | |
| nodeIds: [nodeId], | |
| centralNodeId: nodeId, | |
| score: degree, | |
| }); | |
| } | |
| }); | |
| // Find chain patterns (nodes with degree 2 in sequence) | |
| const visited_chains = new Set<string>(); | |
| Array.from(nodeMap.keys()).forEach((nodeId) => { | |
| if (visited_chains.has(nodeId)) return; | |
| const neighbors = adjacencyList.get(nodeId) || new Set(); | |
| if (neighbors.size === 2) { | |
| // Start building a chain | |
| const chain = [nodeId]; | |
| visited_chains.add(nodeId); | |
| let current: string = nodeId; | |
| let prev: string | null = null; | |
| // Follow the chain | |
| let maxChainLength = 50; // Prevent infinite loops | |
| while (maxChainLength-- > 0) { | |
| const currentNeighbors = Array.from( | |
| adjacencyList.get(current) || new Set<string>() | |
| ); | |
| const next = currentNeighbors.find((n: string) => n !== prev); | |
| if (!next || visited_chains.has(next)) break; | |
| const nextNeighbors = adjacencyList.get(next) || new Set(); | |
| if (nextNeighbors.size > 2) break; // End of chain | |
| chain.push(next); | |
| visited_chains.add(next); | |
| prev = current; | |
| current = next; | |
| if (nextNeighbors.size === 1) break; // End node | |
| } | |
| if (chain.length >= 3) { | |
| structuralPatterns.push({ | |
| type: "chain", | |
| description: `Chain of ${chain.length} nodes`, | |
| nodeIds: chain, | |
| score: chain.length, | |
| }); | |
| } | |
| } | |
| }); | |
| // Find isolated nodes | |
| const isolatedNodes = Array.from(nodeMap.keys()).filter( | |
| (nodeId) => (adjacencyList.get(nodeId) || new Set()).size === 0 | |
| ); | |
| // Calculate network health (0-100) | |
| const healthFactors = { | |
| connectivity: | |
| connectedComponents.length === 1 | |
| ? 100 | |
| : Math.max(0, 100 - connectedComponents.length * 10), | |
| isolation: | |
| isolatedNodes.length === 0 | |
| ? 100 | |
| : Math.max(0, 100 - isolatedNodes.length * 20), | |
| density: Math.min(100, networkDensity * 200), // Scale density to 0-100 | |
| distribution: | |
| avgDegree > 0 | |
| ? Math.min(100, (1 - (maxDegree - avgDegree) / maxDegree) * 100) | |
| : 100, | |
| }; | |
| const networkHealth = Math.round( | |
| healthFactors.connectivity * 0.3 + | |
| healthFactors.isolation * 0.3 + | |
| healthFactors.density * 0.2 + | |
| healthFactors.distribution * 0.2 | |
| ); | |
| // Sort patterns by score | |
| structuralPatterns.sort((a, b) => b.score - a.score); | |
| return { | |
| totalNodes, | |
| totalEdges, | |
| networkDensity: Math.round(networkDensity * 1000) / 1000, | |
| avgDegree, | |
| maxDegree, | |
| connectedComponents: connectedComponents.sort((a, b) => b.size - a.size), | |
| degreeDistribution, | |
| structuralPatterns: structuralPatterns.slice(0, 6), | |
| networkHealth, | |
| isolatedNodes, | |
| }; | |
| }, [state.traces]); | |
| const getHealthColor = (health: number) => { | |
| if (health >= 80) return "text-green-600"; | |
| if (health >= 60) return "text-yellow-600"; | |
| if (health >= 40) return "text-orange-600"; | |
| return "text-red-600"; | |
| }; | |
| const getPatternColor = (type: string) => { | |
| const colorMap: { [key: string]: string } = { | |
| star: "bg-yellow-100 text-yellow-800 border-yellow-200", | |
| hub: "bg-red-100 text-red-800 border-red-200", | |
| chain: "bg-blue-100 text-blue-800 border-blue-200", | |
| cluster: "bg-green-100 text-green-800 border-green-200", | |
| isolated: "bg-gray-100 text-gray-800 border-gray-200", | |
| }; | |
| return colorMap[type] || "bg-gray-100 text-gray-800 border-gray-200"; | |
| }; | |
| const getTypeColor = (index: number) => { | |
| const colors = [ | |
| "bg-blue-500", | |
| "bg-green-500", | |
| "bg-yellow-500", | |
| "bg-purple-500", | |
| "bg-pink-500", | |
| "bg-indigo-500", | |
| ]; | |
| return colors[index % colors.length]; | |
| }; | |
| if (structuralData.totalNodes === 0) { | |
| return ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Network className="h-5 w-5" /> | |
| Structural Connectivity Analysis | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-center py-8 text-muted-foreground"> | |
| <Network className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <p>No structural data available</p> | |
| <p className="text-sm"> | |
| Generate some knowledge graphs to see connectivity analysis | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Summary Stats */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <Card> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-blue-100"> | |
| <Network className="h-4 w-4 text-blue-600" /> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Network Density</p> | |
| <p className="text-2xl font-bold"> | |
| {structuralData.networkDensity} | |
| </p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-green-100"> | |
| <Target className="h-4 w-4 text-green-600" /> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Components</p> | |
| <p className="text-2xl font-bold"> | |
| {structuralData.connectedComponents.length} | |
| </p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-purple-100"> | |
| <BarChart3 className="h-4 w-4 text-purple-600" /> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Avg Degree</p> | |
| <p className="text-2xl font-bold">{structuralData.avgDegree}</p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-orange-100"> | |
| <Activity className="h-4 w-4 text-orange-600" /> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground">Network Health</p> | |
| <p | |
| className={`text-2xl font-bold ${getHealthColor( | |
| structuralData.networkHealth | |
| )}`} | |
| > | |
| {structuralData.networkHealth}% | |
| </p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| {/* Connected Components */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Target className="h-5 w-5" /> | |
| Connected Components | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-3"> | |
| {structuralData.connectedComponents | |
| .slice(0, 6) | |
| .map((component, index) => ( | |
| <div | |
| key={component.id} | |
| className="flex items-center justify-between p-3 rounded-lg border" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div | |
| className={`w-2 h-2 rounded-full ${getTypeColor( | |
| index | |
| )}`} | |
| /> | |
| <div> | |
| <p className="font-medium"> | |
| Component {component.id + 1} | |
| </p> | |
| <p className="text-sm text-muted-foreground"> | |
| {component.size} nodes • Density: {component.density} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 flex-shrink-0"> | |
| <Badge variant="outline"> | |
| Max: {component.maxDegree} | |
| </Badge> | |
| <Badge variant="outline"> | |
| Avg: {component.avgDegree} | |
| </Badge> | |
| </div> | |
| </div> | |
| ))} | |
| {structuralData.isolatedNodes.length > 0 && ( | |
| <div className="flex items-center justify-between p-3 rounded-lg border border-dashed border-gray-300"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-2 h-2 rounded-full bg-gray-400" /> | |
| <div> | |
| <p className="font-medium text-gray-600"> | |
| Isolated Nodes | |
| </p> | |
| <p className="text-sm text-muted-foreground"> | |
| {structuralData.isolatedNodes.length} disconnected nodes | |
| </p> | |
| </div> | |
| </div> | |
| <Badge className="bg-gray-100 text-gray-800 border-gray-200"> | |
| Isolated | |
| </Badge> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Structural Patterns */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <BarChart3 className="h-5 w-5" /> | |
| Structural Patterns | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-3"> | |
| {structuralData.structuralPatterns.length > 0 ? ( | |
| structuralData.structuralPatterns.map((pattern, index) => ( | |
| <div | |
| key={index} | |
| className="flex items-center justify-between p-3 rounded-lg border" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div | |
| className={`w-2 h-2 rounded-full ${getTypeColor( | |
| index | |
| )}`} | |
| /> | |
| <div> | |
| <p className="font-medium capitalize"> | |
| {pattern.type} Pattern | |
| </p> | |
| <p | |
| className="text-sm text-muted-foreground truncate" | |
| title={pattern.description} | |
| > | |
| {pattern.description} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 flex-shrink-0"> | |
| <Badge className={getPatternColor(pattern.type)}> | |
| {pattern.type.toUpperCase()} | |
| </Badge> | |
| <Badge variant="outline"> | |
| {Math.round(pattern.score)} | |
| </Badge> | |
| </div> | |
| </div> | |
| )) | |
| ) : ( | |
| <div className="text-center py-4 text-muted-foreground"> | |
| <p className="text-sm">No significant patterns detected</p> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Degree Distribution */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Activity className="h-5 w-5" /> | |
| Degree Distribution | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| {Object.entries(structuralData.degreeDistribution) | |
| .sort(([a], [b]) => parseInt(a) - parseInt(b)) | |
| .slice(0, 8) | |
| .map(([degree, stats], index) => ( | |
| <div key={degree} className="p-4 rounded-lg border"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div | |
| className={`w-3 h-3 rounded-full ${getTypeColor(index)}`} | |
| /> | |
| <h4 className="font-medium">Degree {degree}</h4> | |
| </div> | |
| <div className="space-y-1 text-sm"> | |
| <div className="flex justify-between"> | |
| <span className="text-muted-foreground">Nodes:</span> | |
| <span>{stats.count}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-muted-foreground">Percentage:</span> | |
| <span>{stats.percentage}%</span> | |
| </div> | |
| <div className="mt-2"> | |
| <p className="text-muted-foreground text-xs mb-1"> | |
| Entity Types: | |
| </p> | |
| <div className="flex flex-wrap gap-1"> | |
| {stats.entityTypes | |
| .slice(0, 3) | |
| .map((type: string, _typeIndex: number) => ( | |
| <Badge | |
| key={type} | |
| variant="outline" | |
| className="text-xs" | |
| > | |
| {type} | |
| </Badge> | |
| ))} | |
| {stats.entityTypes.length > 3 && ( | |
| <Badge variant="outline" className="text-xs"> | |
| +{stats.entityTypes.length - 3} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } | |