mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
feat(desktop): render Mermaid code blocks in markdown file preview
Salvaged from #40531; surgically reapplied onto current main (i18n'd preview-file.tsx). mermaid dep already present on main. Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
This commit is contained in:
parent
299d5c6603
commit
c0adfd4a67
2 changed files with 99 additions and 1 deletions
|
|
@ -6,7 +6,7 @@ import type {
|
|||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Fragment, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ShikiHighlighter from 'react-shiki'
|
||||
import { Streamdown } from 'streamdown'
|
||||
|
||||
|
|
@ -34,6 +34,10 @@ import { setPreviewDirty } from '@/store/preview-edit'
|
|||
import { $currentCwd } from '@/store/session'
|
||||
import { notifyWorkspaceChanged } from '@/store/workspace-events'
|
||||
|
||||
const MermaidDiagram = lazy(() =>
|
||||
import('@/components/chat/mermaid-block').then(m => ({ default: m.MermaidBlock }))
|
||||
)
|
||||
|
||||
const SHIKI_THEME = { dark: 'github-dark-default', light: 'github-light-default' } as const
|
||||
const TEXT_PREVIEW_MAX_BYTES = 512 * 1024
|
||||
const SOURCE_CHUNK_LINES = 200
|
||||
|
|
@ -291,6 +295,16 @@ function MarkdownCode({ className, children, ...props }: ComponentProps<'code'>)
|
|||
)
|
||||
}
|
||||
|
||||
if (language === 'mermaid') {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<div className="my-3 text-xs text-muted-foreground">Loading diagram…</div>}
|
||||
>
|
||||
<MermaidDiagram chart={String(children).replace(/\n$/, '')} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ShikiHighlighter
|
||||
addDefaultStyles={false}
|
||||
|
|
|
|||
84
apps/desktop/src/components/chat/mermaid-block.tsx
Normal file
84
apps/desktop/src/components/chat/mermaid-block.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
let mermaidPromise: Promise<typeof import('mermaid')> | null = null
|
||||
|
||||
function loadMermaid() {
|
||||
if (!mermaidPromise) {
|
||||
mermaidPromise = import('mermaid').then(async mod => {
|
||||
const m = mod.default
|
||||
|
||||
m.initialize({
|
||||
securityLevel: 'strict',
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default'
|
||||
})
|
||||
|
||||
return mod
|
||||
})
|
||||
}
|
||||
|
||||
return mermaidPromise
|
||||
}
|
||||
|
||||
interface MermaidBlockProps {
|
||||
chart: string
|
||||
}
|
||||
|
||||
export function MermaidBlock({ chart }: MermaidBlockProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [svg, setSvg] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function render() {
|
||||
try {
|
||||
const { default: mermaid } = await loadMermaid()
|
||||
const id = `mermaid-${Math.random().toString(36).slice(2, 10)}`
|
||||
const { svg: rendered } = await mermaid.render(id, chart)
|
||||
|
||||
if (!cancelled) {
|
||||
setSvg(rendered)
|
||||
setError(null)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
setSvg(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void render()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [chart])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="my-3 rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
<div className="mb-1 font-medium text-foreground">Mermaid diagram error</div>
|
||||
<pre className="whitespace-pre-wrap break-words text-[0.75rem]">{error}</pre>
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-muted-foreground/70 hover:text-foreground">View source</summary>
|
||||
<pre className="mt-1 whitespace-pre-wrap break-words text-[0.75rem]">{chart}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!svg) {
|
||||
return <div className="my-3 text-xs text-muted-foreground">Rendering diagram…</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="my-3 overflow-auto rounded-md border border-border bg-background p-3 [&_svg]:max-w-full"
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue