mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: render mermaid diagrams in chat code blocks (#9738)
* feat: render mermaid diagrams in chat code blocks Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: guard mermaid render against out-of-order async and transient streaming failures Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: only show mermaid diagram while it matches current source Addresses Codex review: keeping the last good SVG through parse failures left a stale, mismatched diagram on screen when the source changed to something invalid. Tie the rendered SVG to the source that produced it and only display it while it still matches the current code, falling back to the raw source otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+689
-76
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,7 @@
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.540.0",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"mermaid": "^11.15.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0",
|
||||
"monaco-languageclient": "10.6.0",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { AIMode } from '../AIChatManager.svelte'
|
||||
import { getAiChatManager } from '../aiChatManagerContext'
|
||||
import { Check, Play } from 'lucide-svelte'
|
||||
import MermaidDisplay from './MermaidDisplay.svelte'
|
||||
|
||||
const aiChatManager = getAiChatManager()
|
||||
|
||||
@@ -108,14 +109,18 @@
|
||||
<div
|
||||
class="relative w-full border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden"
|
||||
>
|
||||
<HighlightCode
|
||||
className="p-1"
|
||||
code={code ?? ''}
|
||||
highlightLanguage={SMART_LANG_TO_HIGHLIGHT_LANG[getSmartLang(language as string)]}
|
||||
language={undefined}
|
||||
onApplyCode={handleApplyCode}
|
||||
{showApplyButton}
|
||||
applyButtonIcon={aiChatManager.pendingNewCode ? Check : Play}
|
||||
/>
|
||||
{#if language === 'mermaid'}
|
||||
<MermaidDisplay code={code ?? ''} />
|
||||
{:else}
|
||||
<HighlightCode
|
||||
className="p-1"
|
||||
code={code ?? ''}
|
||||
highlightLanguage={SMART_LANG_TO_HIGHLIGHT_LANG[getSmartLang(language as string)]}
|
||||
language={undefined}
|
||||
onApplyCode={handleApplyCode}
|
||||
{showApplyButton}
|
||||
applyButtonIcon={aiChatManager.pendingNewCode ? Check : Play}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { randomUUID } from '$lib/utils/uuid'
|
||||
import { useIsDarkMode } from '$lib/components/DarkModeObserver.svelte'
|
||||
|
||||
let { code }: { code: string } = $props()
|
||||
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
let svg = $state<string | undefined>(undefined)
|
||||
// The exact source that produced `svg`. The diagram is only shown while this
|
||||
// still matches the current `code`, so a later edit that fails to parse falls
|
||||
// back to the raw source instead of leaving a stale, mismatched diagram.
|
||||
let renderedCode = $state<string | undefined>(undefined)
|
||||
// Monotonic token so an earlier-started render that resolves late can't
|
||||
// overwrite the result of a newer one (out-of-order async on rapid code/theme changes).
|
||||
let renderSeq = 0
|
||||
|
||||
async function render(source: string, dark: boolean) {
|
||||
const seq = ++renderSeq
|
||||
if (!source?.trim()) {
|
||||
svg = undefined
|
||||
renderedCode = undefined
|
||||
return
|
||||
}
|
||||
try {
|
||||
const mermaid = (await import('mermaid')).default
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: dark ? 'dark' : 'default',
|
||||
securityLevel: 'strict',
|
||||
// Throw on parse errors instead of injecting an orphan error diagram into the DOM.
|
||||
suppressErrorRendering: true
|
||||
})
|
||||
// mermaid.render needs a fresh element id per attempt to avoid id collisions.
|
||||
const result = await mermaid.render(`mermaid-${randomUUID()}`, source)
|
||||
if (seq !== renderSeq) return
|
||||
svg = result.svg
|
||||
renderedCode = source
|
||||
} catch {
|
||||
// Parse failure (often a partial block still streaming in): fall back to the
|
||||
// raw source. `showSvg` already hides any previous diagram since `renderedCode`
|
||||
// no longer matches the current `code`.
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void render(code, isDarkMode.val)
|
||||
})
|
||||
|
||||
// Only show the diagram while it corresponds to the current source.
|
||||
let showSvg = $derived(svg !== undefined && renderedCode === code)
|
||||
</script>
|
||||
|
||||
{#if showSvg}
|
||||
<div class="p-2 flex justify-center overflow-x-auto">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html svg}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Fallback while loading or when rendering fails: show the raw source -->
|
||||
<pre class="overflow-auto max-h-screen text-xs p-2">{code}</pre>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user