2026-02-24 14:59:28 +00:00

140 lines
5.9 KiB
JavaScript

import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import { Loader2, AlertTriangle, ImageOff } from 'lucide-react';
import mermaid from 'mermaid';
import svgPanZoom from 'svg-pan-zoom';
mermaid.initialize(
{
startOnLoad: false, // We will render manually
theme: 'dark', // Apply dark theme globally
// Dark theme configuration similar to Dracula
themeVariables: {
background: '#0f1117',
primaryColor: '#1a1c25', // Node background
primaryTextColor: '#f8f8f2',
primaryBorderColor: '#6272a4',
// lineColor: '#44475a',
secondaryColor: '#2d303e',
tertiaryColor: '#2d303e',
fontSize: '14px',
fontFamily: 'JetBrains Mono, Fira Code, Menlo, Monaco, Courier New, monospace',
// Specific for class diagrams
classText: '#f8f8f2',
},
securityLevel: 'loose', // Allow clicks etc
}
)
export default function GraphViewer({ mermaidGraphText, compilerMessages, isProcessing,
error, onWarningLocationClick }) {
const svgContainerRef = useRef(null);
const [renderError, setRenderError] = useState(null);
const [svg, setSvg] = useState(null);
const graphId = `mermaid-graph-${Date.now()}`;
// Render graph when text or library readiness changes
useEffect(() => {
if (!mermaidGraphText || isProcessing) return
setRenderError(null);
try {
// mermaid.render() returns a promise with the SVG
mermaid.render(graphId, mermaidGraphText)
.then(({ svg, bindFunctions }) => {
if (svgContainerRef.current) {
svgContainerRef.current.innerHTML = svg;
if (bindFunctions) {
bindFunctions(svgContainerRef.current); // For interactivity if any
}
}
setSvg(svg);
})
.catch(err => {
console.error("Mermaid rendering error:", err);
setRenderError(`Diagram error: ${err.message || 'Could not render graph.'}`);
if (svgContainerRef.current) {
svgContainerRef.current.innerHTML = `<div class="text-red-400 p-4">Error rendering diagram. Check console.</div>`;
}
});
} catch (err) {
console.error("Synchronous Mermaid error:", err);
setRenderError(`Diagram error: ${err.message || 'Critical error.'}`);
if (svgContainerRef.current) {
svgContainerRef.current.innerHTML = `<div class="text-red-400 p-4">Critical diagram rendering error.</div>`;
}
}
}, [mermaidGraphText, isProcessing]);
useLayoutEffect(() => {
if (!svgContainerRef.current || !svg) return;
const svgElement = svgContainerRef.current.children[0];
// make svg size to its current size - required so it's not a tiny thing
svgElement.style.width = '100%';
svgElement.style.height = '100%';
svgElement.style.visibility = 'hidden'; // Hide until pan-zoom is applied
svgPanZoom(svgElement, {
controlIconsEnabled: true
})
svgElement.style.visibility = 'visible'; // Show the SVG after pan-zoom is applied
})
let content;
if (isProcessing) {
content = (
<div className="h-full flex flex-col items-center justify-center text-slate-400">
<Loader2 className="h-12 w-12 animate-spin mb-4 text-sky-400" />
<p>{isProcessing ? "Processing Code..." : "Rendering Diagram..."}</p>
</div>
);
} else if (error || renderError) {
content = (
<div className="h-full flex flex-col items-center justify-center text-red-400">
<AlertTriangle className="h-12 w-12 mb-4" />
<p className="font-semibold">Error</p>
<p className="text-sm text-red-500 mt-1">{error || renderError}</p>
</div>
);
} else if (!mermaidGraphText || mermaidGraphText.trim() === '') {
// This condition checks for specific placeholder texts in the mermaid string
content = (
<div className="h-full w-full flex flex-col items-center justify-center text-slate-400">
<ImageOff className="h-16 w-16 mb-4 text-slate-500" />
{compilerMessages.map(message =>
<div className="mt-4 text-sm bg-[#2d303e] p-3 rounded-md text-left max-w w-full"
style={{cursor: 'pointer'}} // as
onClick={() => onWarningLocationClick(message.location)}>
{/*<p className="font-medium text-slate-300">Try a simple class:</p>*/}
<pre className="mt-2 overflow-x-auto text-xs bg-[#0f1117] p-2 rounded-md text-sky-300">
{
message.severity
+ (message.location ? ` (${message.location.line}:${message.location.column})` : '')
+": "+message.message
}
</pre>
</div>
)}
</div>
);
} else {
// The div will be populated by the useEffect hook
content = (
<div ref={svgContainerRef}
dangerouslySetInnerHTML={{__html: svg}}
className="w-full h-full flex justify-center items-center overflow-auto p-2 mermaid-live-container">
</div>
);
}
return (
<div
className="h-full w-full bg-[#0f1117] rounded-lg border border-slate-800 flex justify-center items-center overflow-hidden">
{content}
</div>
);
}