feat(kb): render Mermaid diagrams in Knowledge Base viewer
Installs mermaid v11 and adds a custom ReactMarkdown code renderer that intercepts fenced mermaid blocks and renders them as SVG diagrams using the dark theme. SVGs are made responsive (width: 100%). Non-mermaid code blocks are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -1,7 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import mermaid from 'mermaid';
|
||||
import { X, Download, Loader, AlertCircle, FileText, File } from 'lucide-react';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'dark',
|
||||
darkMode: true,
|
||||
themeVariables: {
|
||||
background: '#0f172a',
|
||||
primaryColor: '#1e3a5f',
|
||||
primaryTextColor: '#e2e8f0',
|
||||
primaryBorderColor: '#0ea5e9',
|
||||
lineColor: '#475569',
|
||||
secondaryColor: '#1a2e1a',
|
||||
tertiaryColor: '#2d1f14',
|
||||
edgeLabelBackground: '#1e293b',
|
||||
clusterBkg: '#1e293b',
|
||||
titleColor: '#e2e8f0',
|
||||
fontFamily: 'monospace'
|
||||
}
|
||||
});
|
||||
|
||||
let mermaidCounter = 0;
|
||||
|
||||
function MermaidDiagram({ code }) {
|
||||
const ref = useRef(null);
|
||||
const [svgError, setSvgError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const id = `mermaid-kb-${++mermaidCounter}`;
|
||||
mermaid.render(id, code)
|
||||
.then(({ svg }) => {
|
||||
if (!cancelled && ref.current) {
|
||||
ref.current.innerHTML = svg;
|
||||
// Make SVG responsive
|
||||
const svgEl = ref.current.querySelector('svg');
|
||||
if (svgEl) {
|
||||
svgEl.removeAttribute('width');
|
||||
svgEl.removeAttribute('height');
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.maxWidth = '100%';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setSvgError(err.message || 'Failed to render diagram');
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [code]);
|
||||
|
||||
if (svgError) {
|
||||
return (
|
||||
<pre style={{ color: '#EF4444', fontSize: '0.75rem', padding: '0.75rem', background: 'rgba(239,68,68,0.1)', borderRadius: '0.375rem', overflowX: 'auto' }}>
|
||||
Mermaid render error: {svgError}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(14,165,233,0.2)', borderRadius: '0.5rem', padding: '1rem', margin: '1rem 0', overflowX: 'auto' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
|
||||
export default function KnowledgeBaseViewer({ article, onClose }) {
|
||||
@@ -167,7 +232,26 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
|
||||
{/* Markdown Rendering */}
|
||||
{isMarkdown && (
|
||||
<div className="markdown-content">
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ inline, className, children }) {
|
||||
const lang = /language-(\w+)/.exec(className || '')?.[1];
|
||||
if (!inline && lang === 'mermaid') {
|
||||
return <MermaidDiagram code={String(children).replace(/\n$/, '')} />;
|
||||
}
|
||||
return (
|
||||
<code
|
||||
className={className}
|
||||
style={inline ? { background: 'rgba(14,165,233,0.15)', padding: '0.1rem 0.3rem', borderRadius: '0.25rem', fontFamily: 'monospace', fontSize: '0.85em' } : {}}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user