feat: add Sandpack-based Mermaid diagram support

This commit is contained in:
constanttime 2025-07-14 22:14:16 +05:30
parent 32081245da
commit 54db3312c5
7 changed files with 332 additions and 424 deletions

1
.gitignore vendored
View file

@ -134,3 +134,4 @@ helm/**/.values.yaml
/.openai/
/.tabnine/
/.codeium
CLAUDE.md

View file

@ -1,187 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';
import { Button } from '@librechat/client';
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
import { ZoomIn, ZoomOut, RefreshCw } from 'lucide-react';
interface MermaidDiagramProps {
content: string;
}
/** Note: this is just for testing purposes, don't actually use this component */
const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
const mermaidRef = useRef<HTMLDivElement>(null);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const [isRendered, setIsRendered] = useState(false);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: 'base',
securityLevel: 'sandbox',
themeVariables: {
background: '#282C34',
primaryColor: '#333842',
secondaryColor: '#333842',
tertiaryColor: '#333842',
primaryTextColor: '#ABB2BF',
secondaryTextColor: '#ABB2BF',
lineColor: '#636D83',
fontSize: '16px',
nodeBorder: '#636D83',
mainBkg: '#282C34',
altBackground: '#282C34',
textColor: '#ABB2BF',
edgeLabelBackground: '#282C34',
clusterBkg: '#282C34',
clusterBorder: '#636D83',
labelBoxBkgColor: '#333842',
labelBoxBorderColor: '#636D83',
labelTextColor: '#ABB2BF',
},
flowchart: {
curve: 'basis',
nodeSpacing: 50,
rankSpacing: 50,
diagramPadding: 8,
htmlLabels: true,
useMaxWidth: true,
padding: 15,
wrappingWidth: 200,
},
});
const renderDiagram = async () => {
if (mermaidRef.current) {
try {
const { svg } = await mermaid.render('mermaid-diagram', content);
mermaidRef.current.innerHTML = svg;
const svgElement = mermaidRef.current.querySelector('svg');
if (svgElement) {
svgElement.style.width = '100%';
svgElement.style.height = '100%';
const pathElements = svgElement.querySelectorAll('path');
pathElements.forEach((path) => {
path.style.strokeWidth = '1.5px';
});
const rectElements = svgElement.querySelectorAll('rect');
rectElements.forEach((rect) => {
const parent = rect.parentElement;
if (parent && parent.classList.contains('node')) {
rect.style.stroke = '#636D83';
rect.style.strokeWidth = '1px';
} else {
rect.style.stroke = 'none';
}
});
}
setIsRendered(true);
} catch (error) {
console.error('Mermaid rendering error:', error);
mermaidRef.current.innerHTML = 'Error rendering diagram';
}
}
};
renderDiagram();
}, [content]);
const centerAndFitDiagram = () => {
if (transformRef.current && mermaidRef.current) {
const { centerView, zoomToElement } = transformRef.current;
zoomToElement(mermaidRef.current as HTMLElement);
centerView(1, 0);
}
};
useEffect(() => {
if (isRendered) {
centerAndFitDiagram();
}
}, [isRendered]);
const handlePanning = () => {
if (transformRef.current) {
const { state, instance } = (transformRef.current as ReactZoomPanPinchRef | undefined) ?? {};
if (!state || !instance) {
return;
}
const { scale, positionX, positionY } = state;
const { wrapperComponent, contentComponent } = instance;
if (wrapperComponent && contentComponent) {
const wrapperRect = wrapperComponent.getBoundingClientRect();
const contentRect = contentComponent.getBoundingClientRect();
const maxX = wrapperRect.width - contentRect.width * scale;
const maxY = wrapperRect.height - contentRect.height * scale;
let newX = positionX;
let newY = positionY;
if (newX > 0) {
newX = 0;
}
if (newY > 0) {
newY = 0;
}
if (newX < maxX) {
newX = maxX;
}
if (newY < maxY) {
newY = maxY;
}
if (newX !== positionX || newY !== positionY) {
instance.setTransformState(scale, newX, newY);
}
}
}
};
return (
<div className="relative h-screen w-screen cursor-move bg-[#282C34] p-5">
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1}
maxScale={4}
limitToBounds={false}
centerOnInit={true}
initialPositionY={0}
wheel={{ step: 0.1 }}
panning={{ velocityDisabled: true }}
alignmentAnimation={{ disabled: true }}
onPanning={handlePanning}
>
{({ zoomIn, zoomOut }) => (
<>
<TransformComponent
wrapperStyle={{ width: '100%', height: '100%', overflow: 'hidden' }}
>
<div
ref={mermaidRef}
style={{ width: 'auto', height: 'auto', minWidth: '100%', minHeight: '100%' }}
/>
</TransformComponent>
<div className="absolute bottom-2 right-2 flex space-x-2">
<Button onClick={() => zoomIn(0.1)} variant="outline" size="icon">
<ZoomIn className="h-4 w-4" />
</Button>
<Button onClick={() => zoomOut(0.1)} variant="outline" size="icon">
<ZoomOut className="h-4 w-4" />
</Button>
<Button onClick={centerAndFitDiagram} variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</>
)}
</TransformWrapper>
</div>
);
};
export default MermaidDiagram;

View file

@ -16,7 +16,6 @@ import { langSubset, preprocessLaTeX } from '~/utils';
import { unicodeCitation } from '~/components/Web';
import { code, a, p } from './MarkdownComponents';
import store from '~/store';
type TContentProps = {
content: string;
isLatestMessage: boolean;

View file

@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useToastContext } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import SandpackMermaidDiagram from './SandpackMermaidDiagram';
import useHasAccess from '~/hooks/Roles/useHasAccess';
import { useFileDownload } from '~/data-provider';
import { useCodeBlockContext } from '~/Providers';
@ -24,6 +25,7 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMath = lang === 'math';
const isMermaid = lang === 'mermaid';
const isSingleLine = typeof children === 'string' && children.split('\n').length === 1;
const { getNextIndex, resetCounter } = useCodeBlockContext();
@ -35,6 +37,8 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
if (isMath) {
return <>{children}</>;
} else if (isMermaid && typeof children === 'string') {
return <SandpackMermaidDiagram content={children} fallbackToCodeBlock />;
} else if (isSingleLine) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>
@ -56,9 +60,12 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMermaid = lang === 'mermaid';
if (lang === 'math') {
return children;
} else if (isMermaid && typeof children === 'string') {
return <SandpackMermaidDiagram content={children} fallbackToCodeBlock />;
} else if (typeof children === 'string' && children.split('\n').length === 1) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>

View file

@ -0,0 +1,297 @@
import React, { memo, useMemo, useState, useEffect } from 'react';
import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled';
import { cn } from '~/utils';
import { sharedOptions } from '~/utils/artifacts';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
interface SandpackMermaidDiagramProps {
content: string;
className?: string;
fallbackToCodeBlock?: boolean;
}
const mermaidDependencies = {
mermaid: '^11.4.1',
'react-zoom-pan-pinch': '^3.6.1',
'copy-to-clipboard': '^3.3.3',
};
const mermaidTemplate = `
import React, { useEffect, useRef, useState } from "react";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import mermaid from "mermaid";
import copy from "copy-to-clipboard";
export default function MermaidDiagram({ content }) {
const containerRef = useRef(null);
const [svgContent, setSvgContent] = useState("");
const [copied, setCopied] = useState(false);
const handleCopy = () => {
copy(content, { format: 'text/plain' });
setCopied(true);
setTimeout(() => setCopied(false), 3000);
};
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: "default"
});
mermaid.render("mermaid-diagram-" + Math.random().toString(36).substr(2, 9), content)
.then(({ svg }) => {
// Remove fixed width/height attributes from SVG
let processedSvg = svg.replace(/\\s(width|height)="[^"]*"/g, '');
// Add responsive styling
processedSvg = processedSvg.replace(
'<svg',
'<svg style="width:100%;height:auto;max-width:100%;" preserveAspectRatio="xMidYMid meet"'
);
setSvgContent(processedSvg);
})
.catch((err) => {
console.error("Mermaid error:", err);
// Show the mermaid code with error message when parsing fails
const errorHtml =
'<div style="padding: 20px; font-family: monospace;">' +
'<div style="color: #dc2626; margin-bottom: 10px; font-weight: bold;">' +
'Mermaid Syntax Error: ' + (err.message || 'Invalid diagram syntax') +
'</div>' +
'<pre style="background: #f3f4f6; padding: 15px; border-radius: 8px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; color: #374151;">' +
content.replace(/</g, '&lt;').replace(/>/g, '&gt;') +
'</pre>' +
'</div>';
setSvgContent(errorHtml);
});
}, [content]);
return (
<div ref={containerRef} style={{ width: "100%", height: "100%", position: "relative", overflow: "hidden" }}>
<TransformWrapper
initialScale={0.3}
minScale={0.2}
maxScale={4}
centerOnInit={true}
centerZoomedOut={true}
wheel={{ step: 0.1 }}
doubleClick={{ disabled: true }}
alignmentAnimation={{ sizeX: 0, sizeY: 0 }}
>
{({ zoomIn, zoomOut, resetTransform }) => (
<>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%"
}}
contentStyle={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<div
dangerouslySetInnerHTML={{ __html: svgContent }}
style={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
/>
</TransformComponent>
<div style={{ position: "absolute", top: "10px", right: "10px", display: "flex", gap: "8px", zIndex: 50 }}>
<button
onClick={handleCopy}
style={{
padding: "8px",
background: "#ffffff",
border: "1px solid #d1d5db",
borderRadius: "6px",
cursor: "pointer",
color: "#374151",
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
transition: "all 0.2s",
width: "36px",
height: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
onMouseEnter={(e) => { e.target.style.background = "#f3f4f6"; e.target.style.boxShadow = "0 2px 4px 0 rgba(0, 0, 0, 0.1)"; }}
onMouseLeave={(e) => { e.target.style.background = "#ffffff"; e.target.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; }}
title={copied ? "Copied!" : "Copy"}
>
{copied ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17L4 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" strokeWidth="2"/>
<path d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5" stroke="currentColor" strokeWidth="2"/>
</svg>
)}
</button>
<button
onClick={() => zoomOut()}
style={{
padding: "8px",
background: "rgba(255, 255, 255, 0.9)",
border: "1px solid #e5e7eb",
borderRadius: "6px",
cursor: "pointer",
fontSize: "18px",
fontWeight: "500",
color: "#374151",
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
transition: "all 0.2s",
width: "40px",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
onMouseEnter={(e) => e.target.style.background = "#f9fafb"}
onMouseLeave={(e) => e.target.style.background = "rgba(255, 255, 255, 0.9)"}
title="Zoom out"
>
</button>
<button
onClick={() => zoomIn()}
style={{
padding: "8px",
background: "rgba(255, 255, 255, 0.9)",
border: "1px solid #e5e7eb",
borderRadius: "6px",
cursor: "pointer",
fontSize: "18px",
fontWeight: "500",
color: "#374151",
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
transition: "all 0.2s",
width: "40px",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
onMouseEnter={(e) => e.target.style.background = "#f9fafb"}
onMouseLeave={(e) => e.target.style.background = "rgba(255, 255, 255, 0.9)"}
title="Zoom in"
>
+
</button>
<button
onClick={() => resetTransform()}
style={{
padding: "8px 12px",
background: "#ffffff",
border: "1px solid #d1d5db",
borderRadius: "6px",
cursor: "pointer",
fontSize: "13px",
fontWeight: "500",
color: "#374151",
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
transition: "all 0.2s"
}}
onMouseEnter={(e) => { e.target.style.background = "#f3f4f6"; e.target.style.boxShadow = "0 2px 4px 0 rgba(0, 0, 0, 0.1)"; }}
onMouseLeave={(e) => { e.target.style.background = "#ffffff"; e.target.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; }}
title="Reset zoom"
>
Reset
</button>
</div>
</>
)}
</TransformWrapper>
</div>
);
}
`;
const SandpackMermaidDiagram = memo(
({ content, className, fallbackToCodeBlock }: SandpackMermaidDiagramProps) => {
const mermaidContent = content || 'graph TD\n A[No content provided] --> B[Error]';
const [hasError, setHasError] = useState(false);
const files = useMemo(
() => ({
'/App.tsx': `import React from 'react';
import MermaidDiagram from './MermaidDiagram';
export default function App() {
const content = \`${mermaidContent.replace(/`/g, '\\`')}\`;
return <MermaidDiagram content={content} />;
}`,
'/MermaidDiagram.tsx': mermaidTemplate,
}),
[mermaidContent],
);
const key = useMemo(() => {
let hash = 0;
for (let i = 0; i < mermaidContent.length; i++) {
hash = (hash << 5) - hash + mermaidContent.charCodeAt(i);
}
return hash.toString(36);
}, [mermaidContent]);
// Check if mermaid content is valid by attempting to parse it
useEffect(() => {
if (!fallbackToCodeBlock) return;
// Simple validation check for common mermaid syntax errors
const trimmedContent = mermaidContent.trim();
const hasValidStart =
/^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|gitGraph)/i.test(
trimmedContent,
);
if (!hasValidStart || trimmedContent.includes('Mermaid Syntax Error')) {
setHasError(true);
}
}, [mermaidContent, fallbackToCodeBlock]);
// If there's an error and fallback is enabled, show as code block
if (fallbackToCodeBlock && hasError) {
return <CodeBlock lang="mermaid" codeChildren={content} allowExecution={false} />;
}
return (
<div
className={cn(
'my-4 overflow-hidden rounded-lg border border-border-light bg-surface-primary',
'dark:border-border-heavy dark:bg-surface-primary-alt',
className,
)}
style={{ height: '400px' }}
>
<SandpackProvider
key={key}
files={files}
options={sharedOptions}
template="react-ts"
customSetup={{ dependencies: mermaidDependencies }}
>
<SandpackPreview
showOpenInCodeSandbox={false}
showRefreshButton={false}
style={{ height: '100%' }}
/>
</SandpackProvider>
</div>
);
},
);
SandpackMermaidDiagram.displayName = 'SandpackMermaidDiagram';
export default SandpackMermaidDiagram;

View file

@ -2,12 +2,37 @@ import { useMemo } from 'react';
import { removeNullishValues } from 'librechat-data-provider';
import type { Artifact } from '~/common';
import { getKey, getProps, getTemplate, getArtifactFilename } from '~/utils/artifacts';
import { getMermaidFiles } from '~/utils/mermaid';
export default function useArtifactProps({ artifact }: { artifact: Artifact }) {
const [fileKey, files] = useMemo(() => {
if (getKey(artifact.type ?? '', artifact.language).includes('mermaid')) {
return ['App.tsx', getMermaidFiles(artifact.content ?? '')];
const mermaidFiles = {
'App.tsx': `import React from 'react';
import MermaidDiagram from './MermaidDiagram';
export default function App() {
const content = \`${(artifact.content ?? '').replace(/`/g, '\\`')}\`;
return <MermaidDiagram content={content} />;
}`,
'MermaidDiagram.tsx': `import React, { useEffect, useRef } from 'react';
import mermaid from 'mermaid';
export default function MermaidDiagram({ content }) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
mermaid.initialize({ startOnLoad: true, theme: 'default' });
mermaid.render('mermaid-diagram', content).then(({ svg }) => {
ref.current.innerHTML = svg;
});
}
}, [content]);
return <div ref={ref} style={{ width: '100%', height: '100%' }} />;
}`,
};
return ['App.tsx', mermaidFiles];
}
const fileKey = getArtifactFilename(artifact.type ?? '', artifact.language);

View file

@ -1,234 +0,0 @@
import dedent from 'dedent';
const mermaid = dedent(`import React, { useEffect, useRef, useState } from "react";
import {
TransformWrapper,
TransformComponent,
ReactZoomPanPinchRef,
} from "react-zoom-pan-pinch";
import mermaid from "mermaid";
import { ZoomIn, ZoomOut, RefreshCw } from "lucide-react";
import { Button } from "/components/ui/button";
interface MermaidDiagramProps {
content: string;
}
const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
const mermaidRef = useRef<HTMLDivElement>(null);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const [isRendered, setIsRendered] = useState(false);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: "base",
themeVariables: {
background: "#282C34",
primaryColor: "#333842",
secondaryColor: "#333842",
tertiaryColor: "#333842",
primaryTextColor: "#ABB2BF",
secondaryTextColor: "#ABB2BF",
lineColor: "#636D83",
fontSize: "16px",
nodeBorder: "#636D83",
mainBkg: '#282C34',
altBackground: '#282C34',
textColor: '#ABB2BF',
edgeLabelBackground: '#282C34',
clusterBkg: '#282C34',
clusterBorder: "#636D83",
labelBoxBkgColor: "#333842",
labelBoxBorderColor: "#636D83",
labelTextColor: "#ABB2BF",
},
flowchart: {
curve: "basis",
nodeSpacing: 50,
rankSpacing: 50,
diagramPadding: 8,
htmlLabels: true,
useMaxWidth: true,
padding: 15,
wrappingWidth: 200,
},
});
const renderDiagram = async () => {
if (mermaidRef.current) {
try {
const { svg } = await mermaid.render("mermaid-diagram", content);
mermaidRef.current.innerHTML = svg;
const svgElement = mermaidRef.current.querySelector("svg");
if (svgElement) {
svgElement.style.width = "100%";
svgElement.style.height = "100%";
const pathElements = svgElement.querySelectorAll("path");
pathElements.forEach((path) => {
path.style.strokeWidth = "1.5px";
});
const rectElements = svgElement.querySelectorAll("rect");
rectElements.forEach((rect) => {
const parent = rect.parentElement;
if (parent && parent.classList.contains("node")) {
rect.style.stroke = "#636D83";
rect.style.strokeWidth = "1px";
} else {
rect.style.stroke = "none";
}
});
}
setIsRendered(true);
} catch (error) {
console.error("Mermaid rendering error:", error);
mermaidRef.current.innerHTML = "Error rendering diagram";
}
}
};
renderDiagram();
}, [content]);
const centerAndFitDiagram = () => {
if (transformRef.current && mermaidRef.current) {
const { centerView, zoomToElement } = transformRef.current;
zoomToElement(mermaidRef.current as HTMLElement);
centerView(1, 0);
}
};
useEffect(() => {
if (isRendered) {
centerAndFitDiagram();
}
}, [isRendered]);
const handlePanning = () => {
if (transformRef.current) {
const { state, instance } = transformRef.current;
if (!state) {
return;
}
const { scale, positionX, positionY } = state;
const { wrapperComponent, contentComponent } = instance;
if (wrapperComponent && contentComponent) {
const wrapperRect = wrapperComponent.getBoundingClientRect();
const contentRect = contentComponent.getBoundingClientRect();
const maxX = wrapperRect.width - contentRect.width * scale;
const maxY = wrapperRect.height - contentRect.height * scale;
let newX = positionX;
let newY = positionY;
if (newX > 0) {
newX = 0;
}
if (newY > 0) {
newY = 0;
}
if (newX < maxX) {
newX = maxX;
}
if (newY < maxY) {
newY = maxY;
}
if (newX !== positionX || newY !== positionY) {
instance.setTransformState(scale, newX, newY);
}
}
}
};
return (
<div className="relative h-screen w-screen cursor-move bg-[#282C34] p-5">
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1}
maxScale={10}
limitToBounds={false}
centerOnInit={true}
initialPositionY={0}
wheel={{ step: 0.1 }}
panning={{ velocityDisabled: true }}
alignmentAnimation={{ disabled: true }}
onPanning={handlePanning}
>
{({ zoomIn, zoomOut }) => (
<>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<div
ref={mermaidRef}
style={{
width: "auto",
height: "auto",
minWidth: "100%",
minHeight: "100%",
}}
/>
</TransformComponent>
<div className="absolute bottom-2 right-2 flex space-x-2">
<Button onClick={() => zoomIn(0.1)} variant="outline" size="icon">
<ZoomIn className="h-4 w-4" />
</Button>
<Button
onClick={() => zoomOut(0.1)}
variant="outline"
size="icon"
>
<ZoomOut className="h-4 w-4" />
</Button>
<Button
onClick={centerAndFitDiagram}
variant="outline"
size="icon"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</>
)}
</TransformWrapper>
</div>
);
};
export default MermaidDiagram;`);
const wrapMermaidDiagram = (content: string) => {
return dedent(`import React from 'react';
import MermaidDiagram from '/components/ui/MermaidDiagram';
export default App = () => (
<MermaidDiagram content={\`${content}\`} />
);
`);
};
export const getMermaidFiles = (content: string) => {
return {
'App.tsx': wrapMermaidDiagram(content),
'index.tsx': dedent(`import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(<App />);
;`),
'/components/ui/MermaidDiagram.tsx': mermaid,
};
};