@@ -89,7 +89,8 @@ export const PreviewPanel = memo(function PreviewPanel({
8989 if ( previewType === 'html' ) return < HtmlPreview content = { content } />
9090 if ( previewType === 'csv' ) return < CsvPreview content = { content } />
9191 if ( previewType === 'svg' ) return < SvgPreview content = { content } />
92- if ( previewType === 'mermaid' ) return < MermaidFilePreview content = { content } />
92+ if ( previewType === 'mermaid' )
93+ return < MermaidFilePreview content = { content } isStreaming = { isStreaming } />
9394
9495 return null
9596} )
@@ -151,6 +152,7 @@ const MarkdownCheckboxCtx = createContext<{
151152 contentRef : React . MutableRefObject < string >
152153 onToggle : ( index : number , checked : boolean ) => void
153154} | null > ( null )
155+ const MermaidStreamingCtx = createContext ( false )
154156
155157/** Carries the resolved checkbox index from LiRenderer to InputRenderer. */
156158const CheckboxIndexCtx = createContext ( - 1 )
@@ -247,17 +249,57 @@ function CalloutBlock({ type, children }: { type: string; children?: React.React
247249 )
248250}
249251
250- const MermaidDiagram = memo ( function MermaidDiagram ( { definition } : { definition : string } ) {
252+ function MermaidSourcePreview ( {
253+ definition,
254+ isRendering,
255+ } : {
256+ definition : string
257+ isRendering : boolean
258+ } ) {
259+ return (
260+ < div className = 'my-4 overflow-hidden rounded-lg border border-[var(--border)]' >
261+ < div className = 'flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-3)] px-3 py-1.5' >
262+ < span className = 'text-[11px] text-[var(--text-tertiary)]' > mermaid</ span >
263+ { isRendering && < span className = 'text-[11px] text-[var(--text-muted)]' > Rendering...</ span > }
264+ </ div >
265+ < div className = 'code-editor-theme bg-[var(--surface-5)]' >
266+ < pre className = 'm-0 overflow-x-auto whitespace-pre p-4 font-mono text-[13px] text-[var(--text-primary)] leading-[1.6]' >
267+ < code > { definition } </ code >
268+ </ pre >
269+ </ div >
270+ </ div >
271+ )
272+ }
273+
274+ const MermaidDiagram = memo ( function MermaidDiagram ( {
275+ definition,
276+ isStreaming = false ,
277+ } : {
278+ definition : string
279+ isStreaming ?: boolean
280+ } ) {
251281 const [ svg , setSvg ] = useState < string | null > ( null )
252282 const [ error , setError ] = useState < string | null > ( null )
253- const idRef = useRef ( `mermaid-${ generateShortId ( 8 ) } ` )
283+ const [ isRendering , setIsRendering ] = useState ( false )
284+ const [ renderedDefinition , setRenderedDefinition ] = useState < string | null > ( null )
285+ const trimmedDefinition = definition . trim ( )
254286
255287 useEffect ( ( ) => {
256288 if ( typeof window === 'undefined' ) return
289+ if ( ! trimmedDefinition ) {
290+ setSvg ( null )
291+ setError ( null )
292+ setIsRendering ( false )
293+ setRenderedDefinition ( null )
294+ return
295+ }
296+
257297 let cancelled = false
298+ const renderDelay = isStreaming ? 150 : 0
258299
259300 async function render ( ) {
260301 try {
302+ setIsRendering ( true )
261303 const { default : mermaid } = await import ( 'mermaid' )
262304 if ( cancelled ) return
263305
@@ -266,27 +308,55 @@ const MermaidDiagram = memo(function MermaidDiagram({ definition }: { definition
266308 securityLevel : 'strict' ,
267309 theme : 'default' ,
268310 } )
311+ mermaid . setParseErrorHandler ?.( ( ) => undefined )
312+
313+ if ( isStreaming ) {
314+ const parsed = await mermaid . parse ( trimmedDefinition , { suppressErrors : true } )
315+ if ( ! parsed ) {
316+ if ( ! cancelled ) {
317+ setError ( null )
318+ }
319+ return
320+ }
321+ } else {
322+ await mermaid . parse ( trimmedDefinition )
323+ }
269324
270- const { svg : rendered } = await mermaid . render ( idRef . current , definition . trim ( ) )
325+ const { svg : rendered } = await mermaid . render (
326+ `mermaid-${ generateShortId ( 8 ) } ` ,
327+ trimmedDefinition
328+ )
271329 if ( ! cancelled ) {
272330 setSvg ( rendered )
331+ setRenderedDefinition ( trimmedDefinition )
273332 setError ( null )
274333 }
275334 } catch ( err ) {
276335 if ( ! cancelled ) {
277- setError ( toError ( err ) . message || 'Failed to render diagram' )
278- setSvg ( null )
336+ if ( isStreaming ) {
337+ setError ( null )
338+ } else {
339+ setError ( toError ( err ) . message || 'Failed to render diagram' )
340+ setSvg ( null )
341+ setRenderedDefinition ( null )
342+ }
343+ }
344+ } finally {
345+ if ( ! cancelled ) {
346+ setIsRendering ( false )
279347 }
280348 }
281349 }
282350
283- setSvg ( null )
284351 setError ( null )
285- render ( )
352+ const timer = window . setTimeout ( ( ) => {
353+ render ( )
354+ } , renderDelay )
286355 return ( ) => {
287356 cancelled = true
357+ window . clearTimeout ( timer )
288358 }
289- } , [ definition ] )
359+ } , [ trimmedDefinition , isStreaming ] )
290360
291361 if ( error ) {
292362 return (
@@ -297,11 +367,20 @@ const MermaidDiagram = memo(function MermaidDiagram({ definition }: { definition
297367 )
298368 }
299369
300- if ( ! svg ) {
301- return < div className = 'my-4 h-[100px] animate-pulse rounded-lg bg-[var(--surface-2)]' />
370+ if ( svg && renderedDefinition === trimmedDefinition ) {
371+ return (
372+ < div className = 'my-4 overflow-auto rounded-lg' dangerouslySetInnerHTML = { { __html : svg } } />
373+ )
374+ }
375+
376+ if ( isStreaming ) {
377+ return < MermaidSourcePreview definition = { definition } isRendering = { isRendering } />
302378 }
303379
304- return < div className = 'my-4 overflow-auto rounded-lg' dangerouslySetInnerHTML = { { __html : svg } } />
380+ if ( ! trimmedDefinition || ! svg ) {
381+ return < div className = 'my-4 h-[100px] animate-pulse rounded-lg bg-[var(--surface-2)]' />
382+ }
383+ return null
305384} )
306385
307386const STATIC_MARKDOWN_COMPONENTS = {
@@ -374,12 +453,13 @@ const STATIC_MARKDOWN_COMPONENTS = {
374453 )
375454 } ,
376455 code : ( { children, className } : { children ?: React . ReactNode ; className ?: string } ) => {
456+ const isMarkdownStreaming = useContext ( MermaidStreamingCtx )
377457 const langMatch = className ?. match ( / l a n g u a g e - ( \w + ) / )
378458 const langRaw = langMatch ?. [ 1 ] ?? ''
379459 const codeString = extractTextContent ( children )
380460
381461 if ( langRaw === 'mermaid' ) {
382- return < MermaidDiagram definition = { codeString } />
462+ return < MermaidDiagram definition = { codeString } isStreaming = { isMarkdownStreaming } />
383463 }
384464
385465 if ( ! codeString ) {
@@ -706,14 +786,16 @@ const MarkdownPreview = memo(function MarkdownPreview({
706786 const body = (
707787 < div ref = { autoScrollRef } className = 'h-full overflow-auto p-6' >
708788 { frontMatterData && < FrontMatterCard data = { frontMatterData } /> }
709- < Streamdown
710- mode = { streamdownMode }
711- remarkPlugins = { REMARK_PLUGINS }
712- rehypePlugins = { REHYPE_PLUGINS }
713- components = { MARKDOWN_COMPONENTS }
714- >
715- { markdownContent }
716- </ Streamdown >
789+ < MermaidStreamingCtx . Provider value = { isStreaming } >
790+ < Streamdown
791+ mode = { streamdownMode }
792+ remarkPlugins = { REMARK_PLUGINS }
793+ rehypePlugins = { REHYPE_PLUGINS }
794+ components = { MARKDOWN_COMPONENTS }
795+ >
796+ { markdownContent }
797+ </ Streamdown >
798+ </ MermaidStreamingCtx . Provider >
717799 </ div >
718800 )
719801
@@ -878,10 +960,10 @@ function SvgPreview({ content }: { content: string }) {
878960 )
879961}
880962
881- function MermaidFilePreview ( { content } : { content : string } ) {
963+ function MermaidFilePreview ( { content, isStreaming } : { content : string ; isStreaming ?: boolean } ) {
882964 return (
883965 < div className = 'h-full overflow-auto p-6' >
884- < MermaidDiagram definition = { content } />
966+ < MermaidDiagram definition = { content } isStreaming = { isStreaming } />
885967 </ div >
886968 )
887969}
0 commit comments