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:
2026-04-02 15:37:00 -06:00
parent 7c0ba41514
commit 2fead2cfef
2 changed files with 87 additions and 2 deletions

View File

@@ -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>
)}