wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
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<HTMLDivElement>(null);
const cyRef = useRef<Core | null>(null);
const layoutRef = useRef<any>(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 (
<div
ref={containerRef}
className={`w-full h-full ${className}`}
style={{
width: width,
height: height,
minHeight: "300px",
border: "1px solid #e2e8f0",
borderRadius: "8px",
backgroundColor: "#fafafa",
}}
/>
);
};
// 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 (
<div
className="flex items-center justify-center text-muted-foreground bg-muted/20 rounded-lg border-2 border-dashed border-muted"
style={{ minHeight: "300px" }}
>
Graph visualization temporarily unavailable
</div>
);
}
return this.props.children;
}
}
export const SimpleGraphVisualizer: React.FC<SimpleGraphVisualizerProps> = ({
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 (
<CytoscapeErrorBoundary onError={handleError}>
<CytoscapeRenderer
key={renderKey}
data={data}
width={width}
height={height}
className={className}
instanceId={instanceId}
/>
</CytoscapeErrorBoundary>
);
};