import React, { useState, useEffect, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ArrowLeft, ZoomIn, ZoomOut, RotateCcw, AlertCircle, } from "lucide-react"; // import { useAgentGraph } from "@/context/AgentGraphContext"; // Not used in this component import { TemporalGraphData, TemporalControlState, TemporalNode, TemporalLink, } from "@/types/temporal"; import { CytoscapeGraphCore, ElementSelectionCallbacks, } from "@/lib/cytoscape-graph-core"; import { TemporalDataAdapter } from "@/lib/graph-data-adapters"; import { createTemporalGraphConfig } from "@/lib/graph-config-factory"; import { TemporalControls } from "../temporal/TemporalControls"; import { ElementInfoSidebar } from "../temporal/ElementInfoSidebar"; interface TemporalGraphVisualizerProps { temporalData: TemporalGraphData; onBack?: () => void; } export const TemporalGraphVisualizer: React.FC< TemporalGraphVisualizerProps > = ({ temporalData, onBack }) => { // const { actions } = useAgentGraph(); // SVG ref removed - using container for Cytoscape const containerRef = useRef(null); const cytoscapeRef = useRef(null); const animationIntervalRef = useRef(null); const dataAdapter = useRef(new TemporalDataAdapter()); const isInitializedRef = useRef(false); const isDestroyingRef = useRef(false); // State for temporal controls const [controlState, setControlState] = useState({ isPlaying: false, currentWindowIndex: 0, animationSpeed: 1.0, showingFullVersion: false, totalWindows: temporalData.windows.length, }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [graphStats, setGraphStats] = useState<{ nodes: number; links: number; label: string; }>({ nodes: 0, links: 0, label: "Loading..." }); // Selection state const [selectedElement, setSelectedElement] = useState< TemporalNode | TemporalLink | null >(null); const [selectedElementType, setSelectedElementType] = useState< "node" | "link" | null >(null); // Load specific window data const loadWindow = useCallback( (windowIndex: number) => { console.log( `TemporalGraphVisualizer: loadWindow called with index ${windowIndex}` ); if ( !cytoscapeRef.current || windowIndex < 0 || windowIndex >= temporalData.windows.length ) { console.warn( `TemporalGraphVisualizer: Invalid window index ${windowIndex} or missing Cytoscape core` ); return; } try { const window = temporalData.windows[windowIndex]; console.log( `TemporalGraphVisualizer: Loading window ${windowIndex}:`, window ); if (!window || (!window.entities && !window.relations)) { console.warn( `TemporalGraphVisualizer: Window ${windowIndex} has no entities or relations` ); setError(`Window ${windowIndex + 1} contains no graph data`); return; } const adaptedData = dataAdapter.current.adapt({ nodes: window.entities, links: window.relations, metadata: window.metadata, }); cytoscapeRef.current.updateGraph(adaptedData, true); setGraphStats({ nodes: window?.entities?.length || 0, links: window?.relations?.length || 0, label: `Window ${windowIndex + 1}`, }); setControlState((prev) => ({ ...prev, currentWindowIndex: windowIndex, showingFullVersion: false, })); console.log( `TemporalGraphVisualizer: Successfully loaded window ${windowIndex}` ); } catch (err) { console.error( `TemporalGraphVisualizer: Error loading window ${windowIndex}:`, err ); setError( `Failed to load window ${windowIndex + 1}: ${ err instanceof Error ? err.message : String(err) }` ); } }, [temporalData] ); // Initialize Cytoscape visualization useEffect(() => { console.log("TemporalGraphVisualizer: useEffect triggered", { containerRef: !!containerRef.current, temporalData: temporalData, }); // Reset initialization state when data changes isInitializedRef.current = false; isDestroyingRef.current = false; // Add a small delay to ensure DOM is fully rendered const initializeVisualization = async () => { try { if ( !containerRef.current || isInitializedRef.current || isDestroyingRef.current ) { console.log( "TemporalGraphVisualizer: Refs not ready or already initialized", { containerRef: !!containerRef.current, isInitialized: isInitializedRef.current, isDestroying: isDestroyingRef.current, } ); return; } isInitializedRef.current = true; // Wait a bit more for layout to settle await new Promise((resolve) => setTimeout(resolve, 200)); const container = containerRef.current; let width = container.clientWidth || 800; // Fallback width let height = container.clientHeight || 600; // Fallback height // For full-screen temporal visualization, use viewport dimensions if container is small if (width < 800 || height < 600) { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Account for header, controls, padding, and sidebar const headerHeight = 80; // Approximate header height const controlsHeight = 60; // Approximate controls height const padding = 32; // Total padding const sidebarWidth = 384; // Approximate sidebar width (w-96 = 384px) width = Math.max(width, viewportWidth - sidebarWidth - 50); // Account for sidebar and margin height = Math.max( height, viewportHeight - headerHeight - controlsHeight - padding ); console.log( "TemporalGraphVisualizer: Using viewport-based dimensions", { viewportWidth, viewportHeight, sidebarWidth, calculatedWidth: width, calculatedHeight: height, } ); } console.log("TemporalGraphVisualizer: Container dimensions", { width, height, clientWidth: container.clientWidth, clientHeight: container.clientHeight, offsetWidth: container.offsetWidth, offsetHeight: container.offsetHeight, scrollWidth: container.scrollWidth, scrollHeight: container.scrollHeight, }); // If container has zero dimensions, try to get parent dimensions let finalWidth = width; let finalHeight = height; if (width === 0 || height === 0) { const parent = container.parentElement; if (parent) { finalWidth = parent.clientWidth || 800; finalHeight = parent.clientHeight || 600; console.log("TemporalGraphVisualizer: Using parent dimensions", { parentWidth: finalWidth, parentHeight: finalHeight, }); } } if (finalWidth === 0 || finalHeight === 0) { console.warn( "TemporalGraphVisualizer: Container has zero dimensions, using fallbacks" ); finalWidth = Math.max(finalWidth, 800); finalHeight = Math.max(finalHeight, 500); // Ensure minimum height } // Ensure minimum dimensions for usability finalWidth = Math.max(finalWidth, 400); finalHeight = Math.max(finalHeight, 400); const config = createTemporalGraphConfig({ width: finalWidth, height: finalHeight, }); console.log( "TemporalGraphVisualizer: Creating CytoscapeGraphCore with config", config ); // Create selection callbacks const selectionCallbacks: ElementSelectionCallbacks = { onNodeSelect: (node: TemporalNode) => { setSelectedElement(node); setSelectedElementType("node"); }, onLinkSelect: (link: TemporalLink) => { setSelectedElement(link); setSelectedElementType("link"); }, onClearSelection: () => { setSelectedElement(null); setSelectedElementType(null); }, }; // Check if container is already being used by Cytoscape if (containerRef.current.hasAttribute("data-cytoscape-active")) { console.warn( "Container already has active Cytoscape instance, skipping initialization" ); return; } // Mark container as active containerRef.current.setAttribute("data-cytoscape-active", "true"); cytoscapeRef.current = new CytoscapeGraphCore( containerRef.current, config, selectionCallbacks ); // Load initial window if (temporalData.windows.length > 0) { console.log( "TemporalGraphVisualizer: Loading initial window", temporalData.windows[0] ); loadWindow(0); } else { console.warn( "TemporalGraphVisualizer: No windows available in temporal data" ); setError("No temporal windows available for visualization"); } console.log( "TemporalGraphVisualizer: Initialization complete, setting loading to false" ); setLoading(false); } catch (err) { console.error( "TemporalGraphVisualizer: Error initializing Cytoscape visualization:", err ); setError( `Failed to initialize visualization: ${ err instanceof Error ? err.message : String(err) }` ); setLoading(false); } }; // Function to check if refs are ready and initialize const checkAndInitialize = () => { if (!containerRef.current) { console.log("TemporalGraphVisualizer: Missing refs, retrying..."); return false; } return true; }; // If refs not ready immediately, set up a retry mechanism if (!checkAndInitialize()) { let retryCount = 0; const maxRetries = 20; const retryInterval = setInterval(() => { retryCount++; console.log( `TemporalGraphVisualizer: Retry attempt ${retryCount}/${maxRetries}` ); if (checkAndInitialize()) { clearInterval(retryInterval); // Continue with initialization initializeVisualization(); } else if (retryCount >= maxRetries) { clearInterval(retryInterval); console.error( "TemporalGraphVisualizer: Max retries reached, giving up" ); setError( "Failed to initialize visualization: DOM elements not ready" ); setLoading(false); } }, 200); return () => { clearInterval(retryInterval); isDestroyingRef.current = true; if (cytoscapeRef.current) { cytoscapeRef.current.destroy(); cytoscapeRef.current = null; } if (animationIntervalRef.current) { clearInterval(animationIntervalRef.current); animationIntervalRef.current = null; } isInitializedRef.current = false; }; } // If refs are ready, initialize immediately const timeoutId = setTimeout(initializeVisualization, 100); // Cleanup return () => { clearTimeout(timeoutId); isDestroyingRef.current = true; if (cytoscapeRef.current) { cytoscapeRef.current.destroy(); cytoscapeRef.current = null; } if (animationIntervalRef.current) { clearInterval(animationIntervalRef.current); animationIntervalRef.current = null; } isInitializedRef.current = false; }; }, [temporalData, loadWindow]); // Handle window resize for full-screen mode useEffect(() => { const handleResize = () => { if (cytoscapeRef.current && containerRef.current) { const container = containerRef.current; let width = container.clientWidth; let height = container.clientHeight; // Use viewport dimensions for better full-screen experience if (width < 800 || height < 600) { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const headerHeight = 80; const controlsHeight = 60; const padding = 32; const sidebarWidth = 384; // Account for sidebar width = Math.max(width, viewportWidth - sidebarWidth - 50); height = Math.max( height, viewportHeight - headerHeight - controlsHeight - padding ); } console.log("TemporalGraphVisualizer: Resizing to", { width, height }); cytoscapeRef.current.resize(width, height); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // Cleanup effect for component unmount useEffect(() => { return () => { // Final cleanup when component unmounts isDestroyingRef.current = true; if (cytoscapeRef.current) { cytoscapeRef.current.destroy(); cytoscapeRef.current = null; } if (animationIntervalRef.current) { clearInterval(animationIntervalRef.current); animationIntervalRef.current = null; } isInitializedRef.current = false; }; }, []); // Load full version const loadFullVersion = useCallback(() => { if (!cytoscapeRef.current || !temporalData.full_kg) { return; } try { console.log("Loading full version KG:", temporalData.full_kg); const adaptedData = dataAdapter.current.adapt({ nodes: temporalData.full_kg.entities, links: temporalData.full_kg.relations, metadata: temporalData.full_kg.metadata, }); cytoscapeRef.current.updateGraph(adaptedData, true); setGraphStats({ nodes: temporalData.full_kg.entities?.length || 0, links: temporalData.full_kg.relations?.length || 0, label: "Full Version", }); setControlState((prev) => ({ ...prev, showingFullVersion: true, })); } catch (err) { console.error("Error loading full version:", err); setError("Failed to load full version"); } }, [temporalData]); // Animation control functions const startAnimation = useCallback(() => { if (controlState.isPlaying || temporalData.windows.length < 2) { return; } setControlState((prev) => ({ ...prev, isPlaying: true, currentWindowIndex: 0, })); loadWindow(0); animationIntervalRef.current = setInterval(() => { setControlState((prev) => { const nextIndex = prev.currentWindowIndex + 1; if (nextIndex >= temporalData.windows.length) { // Animation complete if (animationIntervalRef.current) { clearInterval(animationIntervalRef.current); } return { ...prev, isPlaying: false }; } else { loadWindow(nextIndex); return { ...prev, currentWindowIndex: nextIndex }; } }); }, 2000 / controlState.animationSpeed); }, [ controlState.isPlaying, controlState.animationSpeed, temporalData.windows.length, loadWindow, ]); const stopAnimation = useCallback(() => { if (animationIntervalRef.current) { clearInterval(animationIntervalRef.current); animationIntervalRef.current = null; } setControlState((prev) => ({ ...prev, isPlaying: false })); }, []); const handlePlay = () => startAnimation(); const handlePause = () => stopAnimation(); const handlePrevious = () => { if (controlState.currentWindowIndex > 0) { loadWindow(controlState.currentWindowIndex - 1); } }; const handleNext = () => { if (controlState.currentWindowIndex < temporalData.windows.length - 1) { loadWindow(controlState.currentWindowIndex + 1); } }; const handleWindowChange = (windowIndex: number) => { loadWindow(windowIndex); }; const handleSpeedChange = (speed: number) => { setControlState((prev) => ({ ...prev, animationSpeed: speed })); }; const handleToggleFullVersion = () => { if (controlState.showingFullVersion) { loadWindow(controlState.currentWindowIndex); } else { loadFullVersion(); } }; const handleReset = () => { stopAnimation(); if (cytoscapeRef.current) { cytoscapeRef.current.resetZoom(); } loadWindow(0); }; const handleZoomIn = () => { if (cytoscapeRef.current) { cytoscapeRef.current.zoomIn(); } }; const handleZoomOut = () => { if (cytoscapeRef.current) { cytoscapeRef.current.zoomOut(); } }; const handleResetZoom = () => { if (cytoscapeRef.current) { cytoscapeRef.current.resetZoom(); } }; if (error) { return (

Visualization Error

{error}

); } return (
{/* Header */}

{temporalData.trace_title}

Temporal Knowledge Graph Visualization

{graphStats.nodes} nodes, {graphStats.links} relations {graphStats.label}
{/* Controls */}
{/* Visualization */}
{/* Cytoscape will render here - no SVG needed */} {/* Loading Overlay */} {loading && (

Loading temporal visualization...

)} {/* Zoom Controls */}
{/* Element Info Sidebar */}
); };