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/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
|
"mermaid": "^11.14.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-markdown": "^10.1.0",
|
"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 ReactMarkdown from 'react-markdown';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
import { X, Download, Loader, AlertCircle, FileText, File } from 'lucide-react';
|
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';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
export default function KnowledgeBaseViewer({ article, onClose }) {
|
export default function KnowledgeBaseViewer({ article, onClose }) {
|
||||||
@@ -167,7 +232,26 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
|
|||||||
{/* Markdown Rendering */}
|
{/* Markdown Rendering */}
|
||||||
{isMarkdown && (
|
{isMarkdown && (
|
||||||
<div className="markdown-content">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user