From 48af71d5f10e175bc29f26b7bf34ea01dbc736fb Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 14 Nov 2023 14:30:19 -0800 Subject: [PATCH] Mermaid: vast improvement --- package-lock.json | 1 - package.json | 1 - .../chat/components/message/ChatMessage.tsx | 11 ++- .../components/message/RenderCodeMermaid.tsx | 92 ++++++++++++------- src/modules/aifn/digrams/DiagramsModal.tsx | 2 +- src/modules/aifn/digrams/diagrams.data.ts | 4 +- 6 files changed, 67 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd572cad7..e00137395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "@trpc/server": "^10.43.3", "@vercel/analytics": "^1.1.1", "browser-fs-access": "^0.35.0", - "dompurify": "^3.0.6", "eventsource-parser": "^1.1.1", "idb-keyval": "^6.2.1", "mermaid": "^10.6.1", diff --git a/package.json b/package.json index 9a5c7b8d0..38e7608e4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@trpc/server": "^10.43.3", "@vercel/analytics": "^1.1.1", "browser-fs-access": "^0.35.0", - "dompurify": "^3.0.6", "eventsource-parser": "^1.1.1", "idb-keyval": "^6.2.1", "mermaid": "^10.6.1", diff --git a/src/apps/chat/components/message/ChatMessage.tsx b/src/apps/chat/components/message/ChatMessage.tsx index de71e6047..d79b1e63f 100644 --- a/src/apps/chat/components/message/ChatMessage.tsx +++ b/src/apps/chat/components/message/ChatMessage.tsx @@ -197,7 +197,7 @@ export function ChatMessage(props: { message: DMessage, showDate?: boolean, diffPreviousText?: string, hideAvatars?: boolean, codeBackground?: string, - noMarkdown?: boolean, filterOnlyCode?: boolean, + noMarkdown?: boolean, diagramMode?: boolean, isBottom?: boolean, noBottomBorder?: boolean, isImagining?: boolean, isSpeaking?: boolean, onMessageDelete?: () => void, @@ -467,7 +467,10 @@ export function ChatMessage(props: { ...blockSx, flexGrow: 0, overflowX: 'auto', - ...(!!props.filterOnlyCode && { boxShadow: 'md' }), + ...(!!props.diagramMode && { + // width: '100%', + boxShadow: 'md', + }), }}> {props.showDate === true && ( @@ -489,13 +492,13 @@ export function ChatMessage(props: { {/* sequence of render components, for each Block */} {!errorMessage && parseBlocks(collapsedText, fromSystem, textDiffs) - .filter(block => block.type === 'code' || !props.filterOnlyCode) + .filter(block => !props.diagramMode || block.type === 'code') .map( (block, index) => block.type === 'html' ? : block.type === 'code' - ? + ? : block.type === 'image' ? : block.type === 'latex' diff --git a/src/apps/chat/components/message/RenderCodeMermaid.tsx b/src/apps/chat/components/message/RenderCodeMermaid.tsx index 91dbe9073..b7be73212 100644 --- a/src/apps/chat/components/message/RenderCodeMermaid.tsx +++ b/src/apps/chat/components/message/RenderCodeMermaid.tsx @@ -1,71 +1,93 @@ import * as React from 'react'; -import type { MermaidConfig } from 'mermaid'; import { Box } from '@mui/joy'; -import { SxProps } from '@mui/system'; + +import { appTheme } from '~/common/app.theme'; const RenderMermaidDynamic = React.lazy(async () => { + + // dynamic import const { default: mermaidAPI } = await import('mermaid'); - const { default: DOMPurify } = await import('dompurify'); - const mermaidConfig: MermaidConfig = { + mermaidAPI.initialize({ startOnLoad: false, - theme: 'default', - // ... any other Mermaid configuration options - }; - mermaidAPI.initialize(mermaidConfig); + // gfx options + fontFamily: appTheme.fontFamily.code, + altFontFamily: appTheme.fontFamily.body, + + // style configuration + fontSize: 8, + 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 }, + }); + const MermaidDiagram = React.memo((props: { mermaidCode: string }) => { // state + const isMounted = React.useRef(false); const [svgCode, setSvgCode] = React.useState(null); const [error, setError] = React.useState(null); const mermaidContainerRef = React.useRef(null); + + // [effect] re-render on code changes React.useEffect(() => { - let isMounted = true; - // Generate a unique ID for each diagram - const id = `mermaid-${Math.random().toString(36).substring(2, 9)}`; - - // Render the diagram - - mermaidAPI - .render(id, props.mermaidCode) - .then(renderResult => { - if (!isMounted) return; - const svg = DOMPurify.sanitize(renderResult.svg); - setSvgCode(svg); - // if (mermaidContainerRef.current) - // renderResult.bindFunctions?.(mermaidContainerRef.current); - }) - .catch(error => { - console.error('Mermaid rendering failed:', error); - setError('Mermaid rendering issue: ' + JSON.stringify(error)); - }) - .finally(() => { - // ... - }); + const updateSvgCode = () => + mermaidAPI + .render( + `mermaid-${Math.random().toString(36).substring(2, 9)}`, + props.mermaidCode, + mermaidContainerRef.current!, + ) + .then(({ svg /*, bindFunctions*/ }) => { + if (mermaidContainerRef.current && isMounted.current) { + setSvgCode(svg); + // bindFunctions?.(mermaidContainerRef.current); + } + }) + .catch((error) => + console.error('Mermaid rendering failed:', error), + ); + // mounting state and 'strict mode' debounce + isMounted.current = true; + const timeout = setTimeout(updateSvgCode, 0); return () => { - isMounted = false; + isMounted.current = false; + clearTimeout(timeout); }; }, [props.mermaidCode]); + if (error) return
Error: {error}
; return ( - ); }); - // Assign a displayName to the component for better debugging MermaidDiagram.displayName = 'MermaidDiagram'; return { default: MermaidDiagram }; diff --git a/src/modules/aifn/digrams/DiagramsModal.tsx b/src/modules/aifn/digrams/DiagramsModal.tsx index 418598cbd..72c13ce21 100644 --- a/src/modules/aifn/digrams/DiagramsModal.tsx +++ b/src/modules/aifn/digrams/DiagramsModal.tsx @@ -176,7 +176,7 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi {!!message && (!abortController || showOptions) && ( setMessage({ ...message, text })} sx={{ diff --git a/src/modules/aifn/digrams/diagrams.data.ts b/src/modules/aifn/digrams/diagrams.data.ts index 0c4294eb4..11c8001c4 100644 --- a/src/modules/aifn/digrams/diagrams.data.ts +++ b/src/modules/aifn/digrams/diagrams.data.ts @@ -37,10 +37,10 @@ mindmap function mermaidDiagramPrompt(diagramType: DiagramType): { sys: string, usr: string } { let promptDetails = diagramType === 'auto' - ? 'You create a valid Mermaid diagram markdown (```mermaid\\n...), ready to be rendered into a diagram or mindmap. Ensure the code contains no external references, and all names are properly enclosed in double quotes and escaped if necessary. Choose the most suitable diagram type from the following supported types: flowchart, sequence, class, state, erd, gantt, pie, git, or mindmap.' + ? 'You create a valid Mermaid diagram markdown (```mermaid\\n...), ready to be rendered into a diagram. Ensure the code contains no external references, and all names are properly enclosed in double quotes and escaped if necessary. Choose the most suitable diagram type from the following supported types: flowchart, sequence, class, state, erd, gantt, pie, git.' : 'You create a valid Mermaid mindmap markdown (```mermaid\\n...), ready to be rendered into a mind map. Ensure the code contains no external references, and all names are properly enclosed in double quotes and escaped if necessary. For example:\n' + mermaidMindmapExample + '\n'; return { - sys: `You are an AI that generates Mermaid code based on provided text. ${promptDetails}`, + sys: `You are an AI that generates correct Mermaid code based on provided text. ${promptDetails}`, usr: `Generate the Mermaid code for a ${diagramType === 'auto' ? 'suitable diagram' : 'mind map'} that represents the preceding assistant message.`, }; }