import React, { useEffect, useState, useCallback, useMemo, useRef, } from "react"; import { Button } from "@/components/ui/button"; import { ZoomIn, ZoomOut, RotateCcw } from "lucide-react"; import { KnowledgeGraph, OptimizationRecommendation } from "@/types"; import { UniversalGraphData, UniversalNode, UniversalLink, } from "@/types/graph-visualization"; import { getGraphDataAdapter } from "@/lib/graph-data-adapters"; import { createKnowledgeGraphConfig } from "@/lib/graph-config-factory"; import { SchemaCapabilities, detectSchemaType, getSchemaCapabilities, } from "@/lib/schema-detection"; import { CytoscapeGraphCore } from "@/lib/cytoscape-graph-core"; import { GraphSelectionCallbacks } from "@/types/graph-visualization"; import { ElementInfoSidebar } from "@/components/features/temporal/ElementInfoSidebar"; import { GraphLegend } from "@/components/shared/GraphLegend"; import { useAgentGraph } from "@/context/AgentGraphContext"; import { api } from "@/lib/api"; interface KnowledgeGraphVisualizerProps { knowledgeGraph: KnowledgeGraph; } export const KnowledgeGraphVisualizer: React.FC< KnowledgeGraphVisualizerProps > = ({ knowledgeGraph: kg }) => { const containerRef = useRef(null); const cytoscapeRef = useRef(null); const [graphData, setGraphData] = useState({ nodes: [], links: [], }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [containerDimensions, setContainerDimensions] = useState({ width: 800, height: 600, }); // Failures extracted from graph data for system-level overview const [failures, setFailures] = useState< { id: string; risk_type: string; description: string; }[] >([]); const [optimizations, setOptimizations] = useState< OptimizationRecommendation[] >([]); // Selection state const [selectedElement, setSelectedElement] = useState< UniversalNode | UniversalLink | null >(null); const [selectedElementType, setSelectedElementType] = useState< "node" | "link" | "failure" | null >(null); // Trace viewer state const { state: agState } = useAgentGraph(); const [traceLines, setTraceLines] = useState([]); const [highlightRanges, setHighlightRanges] = useState< { start: number; end: number }[] >([]); // Schema capabilities state const [schemaCapabilities, setSchemaCapabilities] = useState({ supportsLineReferences: false, supportsFailures: false, supportsImportanceLevels: false, supportsInteractionPrompts: false, }); const dataAdapter = useMemo(() => getGraphDataAdapter(), []); // Helper function to find affected element by ID with smart matching const findAffectedElement = useCallback( (affectedId: string, graphData: UniversalGraphData) => { // Strategy 1: Try exact ID match first let affectedElement = graphData.nodes.find((n) => n.id === affectedId) || graphData.links.find((l) => l.id === affectedId); // Strategy 2: If not found, try smart matching based on ID pattern if (!affectedElement) { const match = affectedId.match( /^(task|tool|agent|input|output|human)_(\d+)$/i ); if (match) { const [, type, index] = match; if (type && index) { const targetIndex = parseInt(index) - 1; // Convert to 0-based index // Find nodes of the matching type const nodesOfType = graphData.nodes.filter( (n) => n.type && n.type.toLowerCase() === type.toLowerCase() ); // Get the node at the specified index if (nodesOfType[targetIndex]) { affectedElement = nodesOfType[targetIndex]; console.log( `Found element by type matching: ${type}[${targetIndex}] -> ${affectedElement.id}` ); } } } } // Strategy 3: If still not found, try partial name matching if (!affectedElement) { affectedElement = graphData.nodes.find( (n) => n.name && n.name.toLowerCase().includes(affectedId.toLowerCase()) ) || graphData.links.find( (l) => l.label && l.label.toLowerCase().includes(affectedId.toLowerCase()) ); if (affectedElement) { console.log( `Found element by partial name matching: ${affectedId} -> ${affectedElement.id}` ); } } return affectedElement; }, [] ); // Fetch graph data from API const fetchGraphData = useCallback(async () => { try { setLoading(true); setError(null); console.log("Fetching graph data for kg_id:", kg.kg_id); // Use the same endpoint as agent_graph_visualizer.html const response = await fetch( `/api/knowledge-graphs/${encodeURIComponent(kg.kg_id)}` ); console.log("API response status:", response.status); if (!response.ok) { const errorText = await response.text(); console.log("API error response:", errorText); throw new Error( `Failed to fetch graph data: ${response.status} - ${errorText}` ); } const data = await response.json(); console.log("API response data:", data); console.log("First entity sample:", data.entities?.[0]); console.log("First relation sample:", data.relations?.[0]); // Convert the API response using our data adapter if (data.entities && data.relations) { console.log( "Using real API data:", data.entities.length, "entities,", data.relations.length, "relations" ); const rawData = await api.knowledgeGraphs.getData( kg.kg_id || kg.filename ); // Detect schema type and set capabilities const schemaType = detectSchemaType(rawData); const capabilities = getSchemaCapabilities(schemaType); setSchemaCapabilities(capabilities); const adapted = dataAdapter.adapt(rawData); const rawFailures = Array.isArray((rawData as any).failures) ? (rawData as any).failures : []; const rawOptimizations = Array.isArray((rawData as any).optimizations) ? (rawData as any).optimizations : []; // Enrich failures with matched element information const enrichedFailures = rawFailures.map((failure: any) => { if (failure.affected_id) { const matchedElement = findAffectedElement( failure.affected_id, adapted ); if (matchedElement) { return { ...failure, matchedElement, displayName: ("name" in matchedElement && matchedElement.name) || ("label" in matchedElement && matchedElement.label) || matchedElement.id, }; } } return failure; }); // Enrich optimizations with matched element information const enrichedOptimizations = rawOptimizations.map((opt: any) => { const affectedNodes = (opt.affected_ids || []).map((id: string) => { const matchedElement = findAffectedElement(id, adapted); return { id, name: matchedElement ? ("name" in matchedElement && matchedElement.name) || ("label" in matchedElement && matchedElement.label) || id : id, }; }); return { ...opt, affected_nodes: affectedNodes }; }); // Debug: Log the data structure to understand the mismatch console.log("Raw failures data:", rawFailures); console.log("Enriched failures:", enrichedFailures); console.log("Raw optimizations data:", rawOptimizations); console.log("Enriched optimizations:", enrichedOptimizations); console.log("Adapted graph nodes:", adapted.nodes.slice(0, 3)); setFailures(enrichedFailures); setOptimizations(enrichedOptimizations); setGraphData(adapted); } else { throw new Error( "API data format unexpected - missing entities or relations" ); } } catch (error) { console.error("Error fetching graph data:", error); setError( `Failed to load graph data: ${ error instanceof Error ? error.message : String(error) }` ); } finally { setLoading(false); } }, [kg.kg_id, kg.filename, dataAdapter]); // Load data on component mount useEffect(() => { fetchGraphData(); }, [fetchGraphData]); // Fetch and number trace once when component mounts or selectedTrace changes useEffect(() => { const fetchTrace = async () => { if (!agState.selectedTrace) return; try { const numbered = await api.traces.getNumberedContent( agState.selectedTrace.trace_id ); setTraceLines(numbered.split("\n")); } catch (err) { console.error("Failed to fetch trace content", err); } }; fetchTrace(); }, [agState.selectedTrace]); // Track container dimensions for draggable legend bounds useEffect(() => { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; setContainerDimensions({ width, height }); } }); if (containerRef.current) { resizeObserver.observe(containerRef.current); // Set initial dimensions const rect = containerRef.current.getBoundingClientRect(); setContainerDimensions({ width: rect.width, height: rect.height }); } return () => { resizeObserver.disconnect(); }; }, []); // Compute highlight ranges when element selection changes useEffect(() => { if (!selectedElement) { setHighlightRanges([]); return; } const sel = selectedElement as any; const refs = [ sel.raw_prompt_ref, sel.raw_text_ref, sel.interaction_prompt_ref, ].find((arr) => Array.isArray(arr) && arr.length > 0); if (!Array.isArray(refs)) { setHighlightRanges([]); return; } const isContinuation = (idx: number): boolean => { if (idx < 0 || idx >= traceLines.length) return false; const line = traceLines[idx] ?? ""; // Strip prefix const contentPart = line.replace(/^\s*/, ""); return contentPart.startsWith(" "); }; const ranges = refs.map((r: any) => { const start = r.line_start - 1; let end = r.line_end - 1; // Extend downward for continuation lines while (end + 1 < traceLines.length && isContinuation(end + 1)) { end += 1; } return { start, end }; }); setHighlightRanges(ranges); }, [selectedElement, traceLines]); // Initialize Cytoscape visualization useEffect(() => { console.log("KnowledgeGraphVisualizer: Cytoscape useEffect triggered", { containerRef: !!containerRef.current, dataNodes: graphData.nodes.length, dataLinks: graphData.links.length, loading, }); if (loading || graphData.nodes.length === 0) { console.log( "KnowledgeGraphVisualizer: Skipping Cytoscape init - still loading or no data" ); return; } const initializeVisualization = async () => { try { if (!containerRef.current) { console.log( "KnowledgeGraphVisualizer: Refs not ready during initialization" ); return; } // Wait for layout to settle await new Promise((resolve) => setTimeout(resolve, 200)); const container = containerRef.current; let width = container.clientWidth || 800; let height = container.clientHeight || 600; // If container has zero dimensions, try to get parent dimensions if (width === 0 || height === 0) { const parent = container.parentElement; if (parent) { width = parent.clientWidth || 800; height = parent.clientHeight || 600; console.log("KnowledgeGraphVisualizer: Using parent dimensions", { parentWidth: width, parentHeight: height, }); } } // Ensure minimum dimensions width = Math.max(width, 500); height = Math.max(height, 400); const config = createKnowledgeGraphConfig({ width, height, showToolbar: false, showSidebar: false, showStats: false, enableSearch: false, enableZoom: true, enablePan: true, enableDrag: true, enableSelection: true, }); console.log( "KnowledgeGraphVisualizer: Creating CytoscapeGraphCore with config", config ); // Create selection callbacks const selectionCallbacks: GraphSelectionCallbacks = { onNodeSelect: (node: UniversalNode) => { console.log("Node selected:", node); console.log("Node raw_prompt:", node.raw_prompt); console.log("Node properties:", node.properties); setSelectedElement(node); setSelectedElementType("node"); }, onLinkSelect: (link: UniversalLink) => { console.log("Link selected:", link); console.log("Link interaction_prompt:", link.interaction_prompt); console.log("Link properties:", link.properties); setSelectedElement(link); setSelectedElementType("link"); }, onClearSelection: () => { console.log("Selection cleared"); setSelectedElement(null); setSelectedElementType(null); }, }; cytoscapeRef.current = new CytoscapeGraphCore( containerRef.current, config, selectionCallbacks ); // Load data console.log("KnowledgeGraphVisualizer: Loading data", { nodes: graphData.nodes.length, links: graphData.links.length, }); cytoscapeRef.current.updateGraph(graphData, true, failures); console.log( "KnowledgeGraphVisualizer: Cytoscape initialization complete" ); } catch (err) { console.error( "KnowledgeGraphVisualizer: Error initializing Cytoscape:", err ); setError( `Failed to initialize visualization: ${ err instanceof Error ? err.message : String(err) }` ); } }; // Function to check if refs are ready and initialize const checkAndInitialize = () => { if (!containerRef.current) { console.log("KnowledgeGraphVisualizer: Missing refs, retrying..."); return false; } return true; }; // If refs not ready immediately, set up a retry mechanism if (!checkAndInitialize()) { let retryCount = 0; const maxRetries = 10; const retryInterval = setInterval(() => { retryCount++; console.log( `KnowledgeGraphVisualizer: Retry attempt ${retryCount}/${maxRetries}` ); if (checkAndInitialize()) { clearInterval(retryInterval); initializeVisualization(); } else if (retryCount >= maxRetries) { clearInterval(retryInterval); console.error( "KnowledgeGraphVisualizer: Max retries reached, giving up" ); setError( "Failed to initialize visualization: DOM elements not ready" ); } }, 200); return () => { clearInterval(retryInterval); if (cytoscapeRef.current) { cytoscapeRef.current.destroy(); cytoscapeRef.current = null; } }; } // If refs are ready, initialize immediately const timeoutId = setTimeout(initializeVisualization, 100); // Cleanup return () => { clearTimeout(timeoutId); if (cytoscapeRef.current) { cytoscapeRef.current.destroy(); cytoscapeRef.current = null; } }; }, [graphData, loading]); // Update graph when failures change to apply failure zones useEffect(() => { if (cytoscapeRef.current && graphData.nodes.length > 0 && !loading) { console.log("KnowledgeGraphVisualizer: Updating graph with failures", { failuresCount: failures.length, failureIds: failures .map((f) => (f as any).affected_id || (f as any).matchedElement?.id) .filter(Boolean), }); cytoscapeRef.current.updateGraph(graphData, false, failures); } }, [failures, graphData, loading]); // Zoom control handlers const handleZoomIn = () => { if (cytoscapeRef.current) { cytoscapeRef.current.zoomIn(); } }; const handleZoomOut = () => { if (cytoscapeRef.current) { cytoscapeRef.current.zoomOut(); } }; const handleResetZoom = () => { if (cytoscapeRef.current) { cytoscapeRef.current.resetZoom(); } }; const handleEntitySelect = (entityId: string) => { if (cytoscapeRef.current) { cytoscapeRef.current.selectNodeById(entityId); } }; if (loading) { return (

Loading Knowledge Graph...

Preparing visualization

); } if (error) { return (
⚠️

Failed to Load Graph

{error}

); } // Debug logging console.log("KnowledgeGraphVisualizer render:", { loading, error, graphDataNodes: graphData.nodes.length, graphDataLinks: graphData.links.length, }); return (
{/* Visualization with sidebars */}
{/* Graph container */}
{/* Draggable Graph Legend */} {/* Zoom Controls - Top Right */}
{/* Right sidebar with all tabs - always show */} { console.log("Failure selected:", failure); // Find the affected element using our smart matching let matchedElement = null; if ((failure as any).affected_id) { matchedElement = findAffectedElement( (failure as any).affected_id, graphData ); if (matchedElement) { console.log("Matched element for failure:", matchedElement); // Set the matched element as selected instead of the failure setSelectedElement(matchedElement); setSelectedElementType("node"); } else { // Fallback to failure if no element found setSelectedElement(failure); setSelectedElementType("failure"); } } else { // No affected_id, select the failure setSelectedElement(failure); setSelectedElementType("failure"); } // Auto-select the affected element in the graph if it exists if ((failure as any).affected_id && cytoscapeRef.current) { const affectedId = (failure as any).affected_id; console.log("Looking for affected element with ID:", affectedId); const cy = cytoscapeRef.current.getCytoscape(); if (cy) { // Strategy 1: Try exact ID match first let affectedElement = cy.getElementById(affectedId); // Strategy 2: If not found, try smart matching based on ID pattern if (affectedElement.length === 0) { const match = affectedId.match( /^(task|tool|agent|input|output|human)_(\d+)$/i ); if (match) { const [, type, index] = match; const targetIndex = parseInt(index) - 1; // Convert to 0-based index // Find elements of the matching type const elementsOfType = cy.nodes().filter((node: any) => { const nodeType = node.data("type"); return ( nodeType && nodeType.toLowerCase() === type.toLowerCase() ); }); // Get the element at the specified index if ( elementsOfType.length > targetIndex && targetIndex >= 0 ) { const targetElement = elementsOfType[targetIndex]; if (targetElement) { affectedElement = cy.getElementById(targetElement.id()); console.log( `Found element by type matching: ${type}[${targetIndex}] -> ${targetElement.id()}` ); } } } } // Strategy 3: If still not found, try partial name matching if (affectedElement.length === 0) { const matchingElements = cy.nodes().filter((node: any) => { const nodeName = node.data("name") || node.data("label"); return ( nodeName && nodeName.toLowerCase().includes(affectedId.toLowerCase()) ); }); if (matchingElements.length > 0) { const firstMatch = matchingElements.first(); affectedElement = cy.getElementById(firstMatch.id()); console.log( `Found element by partial name matching: ${affectedId} -> ${firstMatch.id()}` ); } } if (affectedElement.length > 0) { console.log("Found affected element in graph, selecting it"); // Clear current selection cy.elements().unselect(); // Select the affected element affectedElement.select(); // Center the view on the affected element cy.center(affectedElement); // Optional: Zoom to the element for better visibility cy.fit(affectedElement, 100); // 100px padding } else { console.log( "Affected element not found in graph:", affectedId ); console.log( "Available graph element IDs:", cy .elements() .map((el: any) => el.id()) .join(", ") ); } } } }} knowledgeGraph={kg} numberedLines={traceLines} highlightRanges={highlightRanges} showTraceTab={schemaCapabilities.supportsLineReferences} onShowInTrace={(ranges) => { console.log("Show in trace requested with ranges:", ranges); // Set the highlight ranges with a small delay to ensure tab transition is complete setTimeout(() => { setHighlightRanges(ranges); }, 100); }} onEntitySelect={handleEntitySelect} />
); };