diff --git a/web/src/components/MemoContent/CodeBlock.tsx b/web/src/components/MemoContent/CodeBlock.tsx index e22272590..5c53440e3 100644 --- a/web/src/components/MemoContent/CodeBlock.tsx +++ b/web/src/components/MemoContent/CodeBlock.tsx @@ -1,6 +1,7 @@ import { CheckIcon, CopyIcon } from "lucide-react"; import { useState } from "react"; import { cn } from "@/lib/utils"; +import { MermaidBlock } from "./MermaidBlock"; interface PreProps { children?: React.ReactNode; @@ -19,6 +20,15 @@ export const CodeBlock = ({ children, className, ...props }: PreProps) => { const match = /language-(\w+)/.exec(codeClassName); const language = match ? match[1] : ""; + // If it's a mermaid block, render with MermaidBlock component + if (language === "mermaid") { + return ( + + {children} + + ); + } + const handleCopy = async () => { try { await navigator.clipboard.writeText(codeContent); diff --git a/web/src/components/MemoContent/MermaidBlock.tsx b/web/src/components/MemoContent/MermaidBlock.tsx new file mode 100644 index 000000000..3b09fb986 --- /dev/null +++ b/web/src/components/MemoContent/MermaidBlock.tsx @@ -0,0 +1,102 @@ +import mermaid from "mermaid"; +import { observer } from "mobx-react-lite"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; +import { instanceStore, userStore } from "@/store"; +import { resolveTheme } from "@/utils/theme"; + +interface MermaidBlockProps { + children?: React.ReactNode; + className?: string; +} + +/** + * Maps app theme to Mermaid theme + * @param appTheme - The resolved app theme + * @returns Mermaid theme name + */ +const getMermaidTheme = (appTheme: string): "default" | "dark" => { + switch (appTheme) { + case "default-dark": + return "dark"; + case "default": + case "paper": + case "whitewall": + default: + return "default"; + } +}; + +export const MermaidBlock = observer(({ children, className }: MermaidBlockProps) => { + const containerRef = useRef(null); + const [svg, setSvg] = useState(""); + const [error, setError] = useState(""); + + // Extract the code element and its content + const codeElement = children as React.ReactElement; + const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, ""); + + // Get current theme from store (reactive via MobX observer) + // This will automatically trigger re-render when theme changes + const currentTheme = useMemo(() => { + const userTheme = userStore.state.userGeneralSetting?.theme; + const instanceTheme = instanceStore.state.theme; + const theme = userTheme || instanceTheme; + return resolveTheme(theme); + }, [userStore.state.userGeneralSetting?.theme, instanceStore.state.theme]); + + // Render diagram when content or theme changes + useEffect(() => { + const renderDiagram = async () => { + if (!codeContent || !containerRef.current) { + return; + } + + try { + // Generate a unique ID for this diagram + const id = `mermaid-${Math.random().toString(36).substring(7)}`; + + // Get the appropriate Mermaid theme for current app theme + const mermaidTheme = getMermaidTheme(currentTheme); + + // Initialize mermaid with current theme + mermaid.initialize({ + startOnLoad: false, + theme: mermaidTheme, + securityLevel: "strict", + fontFamily: "inherit", + }); + + // Render the mermaid diagram + const { svg: renderedSvg } = await mermaid.render(id, codeContent); + setSvg(renderedSvg); + setError(""); + } catch (err) { + console.error("Failed to render mermaid diagram:", err); + setError(err instanceof Error ? err.message : "Failed to render diagram"); + } + }; + + renderDiagram(); + }, [codeContent, currentTheme]); + + // If there's an error, fall back to showing the code + if (error) { + return ( +
+
Mermaid Error: {error}
+
+          {codeContent}
+        
+
+ ); + } + + return ( +
+ ); +});