import React, { useEffect, useRef, useCallback, useState, useMemo, } from "react"; import cytoscape, { Core } from "cytoscape"; import cola from "cytoscape-cola"; import { UniversalGraphData } from "@/types"; // Register the cola extension cytoscape.use(cola); // Global error handler to catch cytoscape errors const originalConsoleError = console.error; console.error = (...args) => { const message = args.join(" "); if ( message.includes("Cannot read properties of null") && (message.includes("notify") || message.includes("isHeadless")) ) { // Suppress cytoscape null reference errors console.warn("Cytoscape error suppressed:", ...args); return; } originalConsoleError(...args); }; interface SimpleGraphVisualizerProps { data: UniversalGraphData; width?: number; height?: number; className?: string; } // Inner component that can be completely unmounted const CytoscapeRenderer: React.FC<{ data: UniversalGraphData; width: number; height: number; className: string; instanceId: string; }> = ({ data, width, height, className, instanceId }) => { const containerRef = useRef(null); const cyRef = useRef(null); const layoutRef = useRef(null); const isMountedRef = useRef(true); useEffect(() => { isMountedRef.current = true; if (!containerRef.current || !data.nodes.length) return; // Convert data to Cytoscape format using data attributes const elements = [ ...data.nodes.map((node) => { const label = node.label || node.name || node.id; const labelLength = label.length; const sizeCategory = labelLength <= 10 ? "small" : labelLength <= 20 ? "medium" : "large"; return { data: { id: `${instanceId}-${node.id}`, label: label, type: node.type || "default", sizeCategory: sizeCategory, color: node.color || null, }, }; }), ...data.links.map((link) => ({ data: { id: `${instanceId}-${link.id}`, source: `${instanceId}-${ typeof link.source === "string" ? link.source : link.source.id }`, target: `${instanceId}-${ typeof link.target === "string" ? link.target : link.target.id }`, label: link.label || link.type || "", color: link.color || null, }, })), ]; try { // Create Cytoscape instance with error handling cyRef.current = cytoscape({ container: containerRef.current, elements, style: [ { selector: "node", style: { label: "data(label)", "text-valign": "center", "text-halign": "center", "background-color": "#4f46e5", color: "#ffffff", "text-outline-width": 1, "text-outline-color": "#1e293b", "font-size": "16px", "font-weight": 600, "font-family": "system-ui, -apple-system, sans-serif", width: 50, height: 50, "border-width": 2, "border-color": "#1e293b", "text-wrap": "wrap", "text-max-width": "120px", }, }, { selector: "node[sizeCategory='small']", style: { width: 50, height: 50 }, }, { selector: "node[sizeCategory='medium']", style: { width: 65, height: 65 }, }, { selector: "node[sizeCategory='large']", style: { width: 80, height: 80 }, }, { selector: "edge", style: { width: 3, "line-color": "#64748b", "target-arrow-color": "#64748b", "target-arrow-shape": "triangle", "curve-style": "bezier", label: "data(label)", "font-size": "12px", "font-family": "system-ui, -apple-system, sans-serif", color: "#334155", "text-background-color": "#ffffff", "text-background-opacity": 0.9, "text-background-padding": "4px", "text-border-width": 1, "text-border-color": "#e2e8f0", }, }, { selector: "node:selected", style: { "border-width": 4, "border-color": "#0066cc", "background-color": "#1d4ed8", }, }, { selector: "edge:selected", style: { width: 5, "line-color": "#0066cc", "target-arrow-color": "#0066cc", }, }, // Highlighting styles { selector: "node[color='#22c55e']", style: { "background-color": "#22c55e", "border-color": "#16a34a", color: "#ffffff", }, }, { selector: "node[color='#3b82f6']", style: { "background-color": "#3b82f6", "border-color": "#2563eb", color: "#ffffff", }, }, { selector: "node[color='#94a3b8']", style: { "background-color": "#94a3b8", "border-color": "#64748b", color: "#ffffff", }, }, { selector: "node[color='#f59e0b']", style: { "background-color": "#f59e0b", "border-color": "#d97706", color: "#ffffff", }, }, { selector: "edge[color='#22c55e']", style: { "line-color": "#22c55e", "target-arrow-color": "#22c55e", }, }, { selector: "edge[color='#3b82f6']", style: { "line-color": "#3b82f6", "target-arrow-color": "#3b82f6", }, }, { selector: "edge[color='#94a3b8']", style: { "line-color": "#94a3b8", "target-arrow-color": "#94a3b8", }, }, { selector: "edge[color='#f59e0b']", style: { "line-color": "#f59e0b", "target-arrow-color": "#f59e0b", }, }, { selector: "edge[color='#94a3b8']", style: { "line-color": "#94a3b8", "target-arrow-color": "#94a3b8", }, }, ], layout: { name: "cose", // Use cose layout instead of cola for better stability animate: false, // Disable animation to prevent async issues fit: true, padding: 40, randomize: false, componentSpacing: 100, nodeRepulsion: () => 400000, nodeOverlap: 20, idealEdgeLength: () => 150, edgeElasticity: () => 100, nestingFactor: 5, gravity: 80, numIter: 1000, initialTemp: 200, coolingFactor: 0.95, minTemp: 1.0, }, minZoom: 0.2, maxZoom: 3, }); // Store layout reference for cleanup layoutRef.current = cyRef.current.layout({ name: "cose", animate: false, fit: true, padding: 40, }); // Run the layout if (isMountedRef.current) { layoutRef.current.run(); } // Fit the graph after a short delay setTimeout(() => { if (isMountedRef.current && cyRef.current) { cyRef.current.fit(); } }, 100); // Add error handling for cytoscape events cyRef.current.on("*", (event) => { try { // Let cytoscape handle the event normally } catch (error) { console.warn("Cytoscape event error caught and suppressed:", error); event.stopPropagation(); } }); } catch (error) { console.error("Error creating cytoscape instance:", error); cyRef.current = null; layoutRef.current = null; } // Cleanup function return () => { isMountedRef.current = false; // Stop layout first if (layoutRef.current) { try { layoutRef.current.stop(); layoutRef.current = null; } catch (error) { console.warn("Error stopping layout:", error); layoutRef.current = null; } } if (cyRef.current) { try { const cy = cyRef.current; // Stop all layouts first cy.stop(); // Disable interactions cy.userPanningEnabled(false); cy.userZoomingEnabled(false); cy.boxSelectionEnabled(false); cy.autoungrabify(true); cy.autolock(true); // Remove listeners cy.removeAllListeners(); // Remove elements cy.elements().remove(); // Destroy with a small delay to let any pending operations complete setTimeout(() => { try { if (cyRef.current === cy) { cy.destroy(); cyRef.current = null; } } catch (destroyError) { console.warn("Final destroy error:", destroyError); cyRef.current = null; } }, 50); } catch (error) { console.warn("Error during cytoscape cleanup:", error); cyRef.current = null; } } }; }, [data, instanceId, width, height]); return (
); }; // Error boundary component class CytoscapeErrorBoundary extends React.Component< { children: React.ReactNode; onError?: () => void }, { hasError: boolean } > { constructor(props: { children: React.ReactNode; onError?: () => void }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.warn("Cytoscape error caught by boundary:", error, errorInfo); this.props.onError?.(); } render() { if (this.state.hasError) { return (
Graph visualization temporarily unavailable
); } return this.props.children; } } export const SimpleGraphVisualizer: React.FC = ({ data, width = 400, height = 500, className = "", }) => { const [renderKey, setRenderKey] = useState(0); const instanceId = useMemo( () => `cytoscape-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, [renderKey] ); const handleError = useCallback(() => { // Force complete remount on error setTimeout(() => { setRenderKey((prev) => prev + 1); }, 100); }, []); // Force remount when data changes significantly useEffect(() => { setRenderKey((prev) => prev + 1); }, [data]); return ( ); };