feat(renderer): Mermaid diagram rendering with dark theme (M10.7)
- Lazy-loads Mermaid.js on first ```mermaid block detected - Dark theme with Bitcoin orange accent colors - Cached renders to avoid re-rendering on scroll - Error display inline without crashing app Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e7961c608b
commit
7c6a0565d5
@@ -24,6 +24,7 @@
|
|||||||
"katex": "^0.16.33",
|
"katex": "^0.16.33",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"markdown-it": "^14.1.1",
|
"markdown-it": "^14.1.1",
|
||||||
|
"mermaid": "^11.12.3",
|
||||||
"pdfjs-dist": "^5.5.207",
|
"pdfjs-dist": "^5.5.207",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"plyr": "^3.8.4",
|
"plyr": "^3.8.4",
|
||||||
|
|||||||
@@ -326,6 +326,7 @@
|
|||||||
import { computed, ref, nextTick, watch } from 'vue'
|
import { computed, ref, nextTick, watch } from 'vue'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { hasMath, renderMathInHtml } from '@/composables/useMathRenderer'
|
import { hasMath, renderMathInHtml } from '@/composables/useMathRenderer'
|
||||||
|
import { hasMermaid, renderMermaidBlocks } from '@/composables/useMermaidRenderer'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import ContextMenuItem from '@/components/ui/ContextMenuItem.vue'
|
import ContextMenuItem from '@/components/ui/ContextMenuItem.vue'
|
||||||
import type { Message, WebSearchResult } from '@aiui/core/types/message'
|
import type { Message, WebSearchResult } from '@aiui/core/types/message'
|
||||||
@@ -497,14 +498,17 @@ const displayText = computed(() => {
|
|||||||
const baseMarkdown = computed(() => md.render(displayText.value))
|
const baseMarkdown = computed(() => md.render(displayText.value))
|
||||||
const renderedMarkdown = ref('')
|
const renderedMarkdown = ref('')
|
||||||
|
|
||||||
// Render math after markdown, batch on content change
|
// Render math/mermaid after markdown, batch on content change
|
||||||
watch(baseMarkdown, async (html) => {
|
watch(baseMarkdown, async (html) => {
|
||||||
|
let result = html
|
||||||
|
renderedMarkdown.value = html // Show immediately
|
||||||
if (hasMath(displayText.value)) {
|
if (hasMath(displayText.value)) {
|
||||||
renderedMarkdown.value = html // Show immediately
|
result = await renderMathInHtml(result)
|
||||||
renderedMarkdown.value = await renderMathInHtml(html)
|
|
||||||
} else {
|
|
||||||
renderedMarkdown.value = html
|
|
||||||
}
|
}
|
||||||
|
if (hasMermaid(displayText.value)) {
|
||||||
|
result = await renderMermaidBlocks(result)
|
||||||
|
}
|
||||||
|
renderedMarkdown.value = result
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Lazy Mermaid diagram rendering for ```mermaid code blocks.
|
||||||
|
* Dark theme matching glass design, cached renders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let mermaidModule: typeof import('mermaid') | null = null
|
||||||
|
let mermaidLoading: Promise<typeof import('mermaid')> | null = null
|
||||||
|
let mermaidInitialized = false
|
||||||
|
|
||||||
|
async function loadMermaid() {
|
||||||
|
if (mermaidModule) return mermaidModule
|
||||||
|
if (mermaidLoading) return mermaidLoading
|
||||||
|
mermaidLoading = import('mermaid').then((m) => {
|
||||||
|
mermaidModule = m
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
return mermaidLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMermaid() {
|
||||||
|
if (mermaidInitialized || !mermaidModule) return
|
||||||
|
mermaidModule.default.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: 'dark',
|
||||||
|
themeVariables: {
|
||||||
|
primaryColor: '#F7931A',
|
||||||
|
primaryTextColor: '#fff',
|
||||||
|
primaryBorderColor: '#F7931A',
|
||||||
|
lineColor: '#666',
|
||||||
|
secondaryColor: '#1a1a1a',
|
||||||
|
tertiaryColor: '#111',
|
||||||
|
background: '#0a0a0a',
|
||||||
|
mainBkg: '#1a1a1a',
|
||||||
|
nodeBorder: '#444',
|
||||||
|
clusterBkg: '#111',
|
||||||
|
clusterBorder: '#333',
|
||||||
|
titleColor: '#ddd',
|
||||||
|
edgeLabelBackground: '#1a1a1a',
|
||||||
|
},
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
fontSize: 13,
|
||||||
|
})
|
||||||
|
mermaidInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache rendered diagrams
|
||||||
|
const renderCache = new Map<string, string>()
|
||||||
|
|
||||||
|
export function hasMermaid(text: string): boolean {
|
||||||
|
return /```mermaid/i.test(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderCounter = 0
|
||||||
|
|
||||||
|
export async function renderMermaidBlocks(html: string): Promise<string> {
|
||||||
|
await loadMermaid()
|
||||||
|
initMermaid()
|
||||||
|
|
||||||
|
const mermaid = mermaidModule!.default
|
||||||
|
|
||||||
|
// Find <pre><code class="language-mermaid">...</code></pre> blocks
|
||||||
|
const PRE_RE = /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/gi
|
||||||
|
const matches = [...html.matchAll(PRE_RE)]
|
||||||
|
if (matches.length === 0) return html
|
||||||
|
|
||||||
|
let result = html
|
||||||
|
for (const match of matches) {
|
||||||
|
const raw = match[1]
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
const cacheKey = raw
|
||||||
|
if (renderCache.has(cacheKey)) {
|
||||||
|
result = result.replace(match[0], renderCache.get(cacheKey)!)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = `mermaid-${++renderCounter}`
|
||||||
|
const { svg } = await mermaid.render(id, raw)
|
||||||
|
const wrapped = `<div class="mermaid-diagram my-4 overflow-x-auto rounded-lg bg-white/5 border border-white/5 p-4">${svg}</div>`
|
||||||
|
renderCache.set(cacheKey, wrapped)
|
||||||
|
result = result.replace(match[0], wrapped)
|
||||||
|
} catch {
|
||||||
|
// Show error inline without crashing
|
||||||
|
const errHtml = `<div class="my-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400/70">Mermaid render error</div><pre><code>${match[1]}</code></pre>`
|
||||||
|
result = result.replace(match[0], errHtml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Generated
+853
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user