Skip to content

Commit df381e0

Browse files
committed
fix mermaid diagram state mgmt
1 parent 8118035 commit df381e0

2 files changed

Lines changed: 110 additions & 24 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ const IframePreview = memo(function IframePreview({
225225
)
226226

227227
if (streamingContent !== undefined) {
228-
if (!streamingSource || streamingSource.buffer.byteLength === 0) {
228+
if (
229+
!streamingSource ||
230+
streamingSource.kind !== 'buffer' ||
231+
streamingSource.buffer.byteLength === 0
232+
) {
229233
return <div className='relative flex flex-1 overflow-hidden'>{PDF_PAGE_SKELETON}</div>
230234
}
231235
return <PdfViewerCore key={streamingBufferSeq} source={streamingSource} filename={file.name} />

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
156158
const 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

307386
const 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(/language-(\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

Comments
 (0)