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(() => { const nodeMap = new Map< string, { id: string; type: string; name: string } >(); const adjacencyList = new Map>(); const edgeSet = new Set(); // 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(); 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(); 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() ); 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 ( Structural Connectivity Analysis

No structural data available

Generate some knowledge graphs to see connectivity analysis

); } return (
{/* Summary Stats */}

Network Density

{structuralData.networkDensity}

Components

{structuralData.connectedComponents.length}

Avg Degree

{structuralData.avgDegree}

Network Health

{structuralData.networkHealth}%

{/* Connected Components */} Connected Components
{structuralData.connectedComponents .slice(0, 6) .map((component, index) => (

Component {component.id + 1}

{component.size} nodes • Density: {component.density}

Max: {component.maxDegree} Avg: {component.avgDegree}
))} {structuralData.isolatedNodes.length > 0 && (

Isolated Nodes

{structuralData.isolatedNodes.length} disconnected nodes

Isolated
)}
{/* Structural Patterns */} Structural Patterns
{structuralData.structuralPatterns.length > 0 ? ( structuralData.structuralPatterns.map((pattern, index) => (

{pattern.type} Pattern

{pattern.description}

{pattern.type.toUpperCase()} {Math.round(pattern.score)}
)) ) : (

No significant patterns detected

)}
{/* Degree Distribution */} Degree Distribution
{Object.entries(structuralData.degreeDistribution) .sort(([a], [b]) => parseInt(a) - parseInt(b)) .slice(0, 8) .map(([degree, stats], index) => (

Degree {degree}

Nodes: {stats.count}
Percentage: {stats.percentage}%

Entity Types:

{stats.entityTypes .slice(0, 3) .map((type: string, _typeIndex: number) => ( {type} ))} {stats.entityTypes.length > 3 && ( +{stats.entityTypes.length - 3} )}
))}
); }