mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
refactor(desktop): route preview-pane mermaid fences through shared embeds registry
Drop the duplicate mermaid-block.tsx (own mermaid.initialize + render path, theme frozen at first load) and wire preview-file.tsx's MarkdownCode through the existing RichCodeBlock registry from #52935 instead. One mermaid init path, theme-flip re-init, Zoomable + copy-as-PNG, RichBoundary error fallback — and the preview pane gets svg fences for free. Shiki block stays as the fallback for all other languages.
This commit is contained in:
parent
c0adfd4a67
commit
7ff86f4458
2 changed files with 9 additions and 100 deletions
|
|
@ -6,7 +6,7 @@ import type {
|
|||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import { Fragment, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ShikiHighlighter from 'react-shiki'
|
||||
import { Streamdown } from 'streamdown'
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import { requestComposerFocus, requestComposerInsertRefs } from '@/app/chat/comp
|
|||
import { droppedFileInlineRef } from '@/app/chat/composer/inline-refs'
|
||||
import { HERMES_PATHS_MIME } from '@/app/chat/hooks/use-composer-actions'
|
||||
import { isAddSelectionShortcut } from '@/app/right-sidebar/terminal/selection'
|
||||
import { RichCodeBlock } from '@/components/assistant-ui/embeds'
|
||||
import { CodeEditor } from '@/components/chat/code-editor'
|
||||
import { FileDiffPanel } from '@/components/chat/diff-lines'
|
||||
import { chunkTextLines, useFixedRowWindow } from '@/components/chat/fixed-row-window'
|
||||
|
|
@ -34,10 +35,6 @@ 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
|
||||
|
|
@ -295,17 +292,9 @@ 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>
|
||||
)
|
||||
}
|
||||
const code = String(children).replace(/\n$/, '')
|
||||
|
||||
return (
|
||||
const highlighted = (
|
||||
<ShikiHighlighter
|
||||
addDefaultStyles={false}
|
||||
as="div"
|
||||
|
|
@ -315,9 +304,13 @@ function MarkdownCode({ className, children, ...props }: ComponentProps<'code'>)
|
|||
showLanguage={false}
|
||||
theme={SHIKI_THEME}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
{code}
|
||||
</ShikiHighlighter>
|
||||
)
|
||||
|
||||
// ```mermaid / ```svg fences route to the shared lazy renderers (same
|
||||
// registry the chat transcript uses); everything else stays on Shiki.
|
||||
return <RichCodeBlock code={code} fallback={highlighted} language={language} />
|
||||
}
|
||||
|
||||
const MARKDOWN_COMPONENTS = {
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
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