import * as React from 'react'; import { create } from 'zustand'; import { Box } from '@mui/joy'; import { appTheme } from '~/common/app.theme'; import { isBrowser } from '~/common/util/pwaUtils'; /** * We are loading Mermaid from the CDN (and spending all the work to dynamically load it * and strong type it), because the Mermaid dependencies (npm i mermaid) are too heavy * and would slow down development for everyone. * * If you update this file, also make sure the interfaces/type definitions and initialization * options are updated accordingly. */ const MERMAID_CDN_FILE: string = 'https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js'; interface MermaidAPI { initialize: (config: any) => void; render: (id: string, text: string, svgContainingElement?: Element) => Promise<{ svg: string, bindFunctions?: (element: Element) => void }>; } // extend the Window interface, to allow for the mermaid API to be found declare global { // noinspection JSUnusedGlobalSymbols interface Window { mermaid: MermaidAPI; } } interface MermaidAPIStore { mermaidAPI: MermaidAPI | null, loadingError: string | null, } const useMermaidStore = create()( () => ({ mermaidAPI: null, loadingError: null, }), ); let loadingStarted: boolean = false; let loadingError: string | null = null; function loadMermaidFromCDN() { if (isBrowser && !loadingStarted) { loadingStarted = true; const script = document.createElement('script'); script.src = MERMAID_CDN_FILE; script.defer = true; script.onload = () => { useMermaidStore.setState({ mermaidAPI: initializeMermaid(window.mermaid), loadingError: null, }); }; script.onerror = () => { useMermaidStore.setState({ mermaidAPI: null, loadingError: `Script load error for ${script.src}`, }); }; document.head.appendChild(script); } } function initializeMermaid(mermaidAPI: MermaidAPI): MermaidAPI { mermaidAPI.initialize({ startOnLoad: false, // gfx options fontFamily: appTheme.fontFamily.code, altFontFamily: appTheme.fontFamily.body, // style configuration htmlLabels: true, securityLevel: 'loose', theme: 'forest', // per-chart configuration mindmap: { useMaxWidth: false }, flowchart: { useMaxWidth: false }, sequence: { useMaxWidth: false }, timeline: { useMaxWidth: false }, class: { useMaxWidth: false }, state: { useMaxWidth: false }, pie: { useMaxWidth: false }, er: { useMaxWidth: false }, gantt: { useMaxWidth: false }, gitGraph: { useMaxWidth: false }, }); return mermaidAPI; } function useMermaidLoader() { const { mermaidAPI } = useMermaidStore(); React.useEffect(() => { if (!mermaidAPI) loadMermaidFromCDN(); }, [mermaidAPI]); return { mermaidAPI, isSuccess: !!mermaidAPI, isLoading: loadingStarted, error: loadingError }; } export function RenderCodeMermaid(props: { mermaidCode: string }) { // state const [svgCode, setSvgCode] = React.useState(null); const hasUnmounted = React.useRef(false); const mermaidContainerRef = React.useRef(null); // external state const { mermaidAPI, error: mermaidError } = useMermaidLoader(); // [effect] re-render on code changes React.useEffect(() => { if (!mermaidAPI) return; const updateSvgCode = () => { const elementId = `mermaid-${Math.random().toString(36).substring(2, 9)}`; mermaidAPI .render(elementId, props.mermaidCode, mermaidContainerRef.current!) .then(({ svg }) => { if (mermaidContainerRef.current && !hasUnmounted.current) { setSvgCode(svg); // bindFunctions?.(mermaidContainerRef.current); } }) .catch((error) => console.warn('The AI-generated Mermaid code is invalid, please try again. Details below:\n >>', error.message), ); }; // strict-mode de-bounce, plus watch for unmounts hasUnmounted.current = false; const timeout = setTimeout(updateSvgCode, 0); return () => { hasUnmounted.current = true; clearTimeout(timeout); }; }, [mermaidAPI, props.mermaidCode]); // render errors when loading Mermaid. for syntax errors, the Error SVG will be rendered in-place if (mermaidError) return
Error: {mermaidError}
; return ( ); }