Skip to content

feat(markdown): render Mermaid across chat + preview surfaces (#50)#195

Open
MarkSiqiZhang wants to merge 4 commits intofix/issue-50-latex-bracketsfrom
fix/issue-50-mermaid-chat
Open

feat(markdown): render Mermaid across chat + preview surfaces (#50)#195
MarkSiqiZhang wants to merge 4 commits intofix/issue-50-latex-bracketsfrom
fix/issue-50-mermaid-chat

Conversation

@MarkSiqiZhang
Copy link
Copy Markdown
Collaborator

@MarkSiqiZhang MarkSiqiZhang commented Apr 17, 2026

Summary

Fixes Issue #50: render ```mermaid fences as SVG diagrams in every markdown surface, not just chat. Stacked on #191 (LaTeX bracket delimiters); base will auto-retarget to main once #191 merges.

Changes

Chat surface — 7794e44

  • New MermaidBlock component — lazy-loads mermaid@11 via dynamic import, 150 ms debounce so streaming partials don't flash a parse error, graceful <pre> fallback on invalid syntax, copy-source button. useTheme() drives the diagram theme so dark-mode toggles repaint live.
  • Hooks into chat's existing CodeBlock via a 3-line short-circuit when language === 'mermaid'.
  • Tightens formatFileTreeInContent: skip detection inside fenced code blocks; only wrap when the block contains at least one ├── or └──. Lone- clusters (ASCII pipelines, Mermaid-ish art) now pass through unwrapped.

Preview surfaces — c06f69a

  • New shared src/components/shared/MarkdownRenderer.tsx — owns mermaid routing, normalizeLatexDelimiters, plugin defaults (remarkGfm, remarkMath, rehypeKatex), and code-block rendering. Two code-block variants:
    • codeBlockStyle="syntax" — lazy-loaded react-syntax-highlighter + copy button (used by CodeEditor + ChatContextFilePreview).
    • codeBlockStyle="plain" — language banner + <pre>, matching ResearchLab's pre-existing look.
  • Migrated three surfaces:
    • CodeEditor.jsx markdown preview tab — the bug in the screenshot.
    • chat/view/subcomponents/ChatContextFilePreview.tsx — was bare <ReactMarkdown> with no components prop.
    • ResearchLab.jsx IdeaCard body + FileViewer markdown preview.
  • Net diff: −150 / +20 in the migrated files; ~250 lines added in the new shared component.

Scope decisions

  • chat/view/subcomponents/Markdown.tsx intentionally deferred. It already renders mermaid correctly (per 7794e44) and has streaming highlighting + onFileOpen link rewriting + insight callouts that need to be lifted into props before consolidation. Migrating it in this PR would add regression risk without fixing any user-visible bug. Tracked as a follow-up.
  • ResearchLab's OVERVIEW_MARKDOWN_COMPONENTS unchanged — small inline blurbs with intentionally minimal styling. Mermaid is unlikely to appear there.

i18n

No new keys. Reuses chat:codeBlock.{copy,copied,rendering,renderError} (already present in en/zh-CN/ko from 7794e44).

Bundle

  • Mermaid stays code-split (mermaid.core-*.js + per-diagram chunks).
  • react-syntax-highlighter is now React.lazy()-loaded inside MarkdownRenderer, so surfaces using codeBlockStyle="plain" (ResearchLab) don't pull it.

Test plan

npm run typecheck and npm run build must pass. Then npm run dev and exercise the fixture below in each surface.

Fixture (any .md file):

# Mermaid test

```mermaid
flowchart TD
  A[Start] --> B{Decision}
  B -->|yes| C[Render]
  B -->|no| D[Fallback]
```

```python
def hello(): print("hi")
```

Inline `code` and $E = mc^2$.

| a | b |
|---|---|
| 1 | 2 |

```mermaid
broken syntax
```

Per surface:

  • Chat (regression) — paste in a chat message; SVG renders; toggle dark mode → diagram repaints; broken block falls back to <pre> with amber error.
  • CodeEditor preview — open as .md, click preview toggle; SVG renders; python keeps syntax highlighting; KaTeX renders; broken block → fallback.
  • ChatContextFilePreview — attach the .md as chat context, expand preview; same checks as CodeEditor.
  • ResearchLab — open an idea card containing mermaid; open the FileViewer markdown preview; SVG renders; python uses plain <pre> (no syntax highlighter — verifies the codeBlockStyle="plain" opt-out path).
  • File-tree wrap regression — ASCII pipeline (only ) is no longer auto-wrapped in ```text; a real file tree (├──, └──) still wraps.
  • LaTeX regression\[ r_t(\theta) = ... \] still renders via KaTeX; no \theta corruption.

MarkSiqiZhang and others added 2 commits April 17, 2026 15:21
Add a MermaidBlock component that renders ```mermaid fences inline as
SVG via a lazy-loaded mermaid@11 import. The chat Markdown CodeBlock
short-circuits to MermaidBlock when language === 'mermaid', bypassing
syntax highlighting. Streaming partial sources are debounced 150ms and
fall back to a <pre> of the raw source on parse errors, so users never
see a flashing error during a live stream. Theme is driven by useTheme()
so diagrams repaint live on dark-mode toggle without remount.

Also tighten formatFileTreeInContent so it no longer mis-wraps non-tree
content in ```text fences — the likely cause of the "fence marker / text
label leak" symptom in issue #50. Two guardrails:

  1. Skip detection inside fenced code blocks (insideFence toggle).
  2. Only commit the wrap when the collected block contains at least
     one strong signal (├── or └──); lone-│ clusters (ASCII pipelines,
     Mermaid-ish art) pass through unwrapped.

i18n: add codeBlock.rendering / codeBlock.renderError in en/zh-CN/ko;
reuse existing codeBlock.copy / codeBlock.copied for the diagram copy
button.

Scope: chat surface only, mirroring PR #191. ChatContextFilePreview,
ResearchLab, and CodeEditor are deferred to a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urfaces.

Extract a shared MarkdownRenderer that owns mermaid routing + LaTeX normalization + plugin defaults, and migrate three surfaces to it: CodeEditor markdown preview, ChatContextFilePreview, and ResearchLab IdeaCard + FileViewer.
@MarkSiqiZhang MarkSiqiZhang changed the title feat(chat): render Mermaid diagrams + tighten file-tree auto-wrap (#50) feat(markdown): render Mermaid across chat + preview surfaces (#50) Apr 17, 2026
@liuyixin-louis
Copy link
Copy Markdown
Collaborator

我这边本地把这条 PR 看完了,也跑了 npm run typechecknpm run build,这两项都过了。

另外我用浏览器把这次 PR 改到的几条 Mermaid / Markdown 渲染链路单独挂出来测了一轮:

  • shared MarkdownRenderersyntax 路径
  • shared MarkdownRendererplain 路径
  • chat 的 Markdown
  • ChatContextFilePreview

这几条链路里:

  • 正常 Mermaid block 都能渲染成 SVG
  • broken Mermaid 会回退到源码 + error 文案
  • KaTeX / 普通代码块也都还在
  • dark mode toggle 会触发 Mermaid 重绘

不过我抓到一个建议尽快补掉的 follow-up:

src/components/shared/MarkdownRenderer.tsx 现在只接管了 code,但没有接管 pre。对于 fenced code block,react-markdown 还会保留外层 <pre>,所以现在实际 DOM 会变成类似:

  • <pre><div ...></div></pre>
  • <pre><pre ...></pre></pre>

我本地能稳定看到 React 的 validateDOMNesting warning,受影响的是这次迁过去的 preview surfaces。这个点功能上不一定立刻炸,但实现上是脏的,后面很容易演化成布局 / 样式 / 可访问性问题。

建议这里把 pre 也一起接管掉,比如给 shared renderer 补一个 pre: ({ children }) => <>{children}</>,或者等价地把 block code 的容器统一提到 pre 层处理。

除了这个点之外,我这边暂时没有看到别的明显 blocker。

MarkSiqiZhang and others added 2 commits April 21, 2026 23:51
…down v10

react-markdown@10 removed the `inline` prop and now passes HAST nodes
(`node.type === 'element'`) instead of MDAST (`node.type === 'inlineCode'`),
so `inlineDetected` was always false — every inline `<code>` in a paragraph
fell through to the block renderer and produced `<p><div>...</div></p>`
DOM, tripping validateDOMNesting warnings. Add a newline-based fallback:
markdown forbids newlines in inline code and block children always carry a
trailing \n, so `!/[\r\n]/.test(raw)` reliably distinguishes the two.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MarkSiqiZhang
Copy link
Copy Markdown
Collaborator Author

感谢 review,这里现在修复了。

  1. src/components/shared/MarkdownRenderer.tsxdefaultMarkdownComponents 里加了 pre: ({ children }) => <>{children}</>。这样 react-markdown 不再给 fenced code block 套外层 <pre>,原来的 <pre><div>…</div></pre> / <pre><pre>…</pre></pre> 的问题就修复了。(commit 3b5b62f)
  2. 顺手发现的另一条导致 validateDOMNesting 的Bug:
    原因是 buildCodeRenderer 里这行:const inlineDetected = inline || (node && node.type === 'inlineCode');
    在 react-markdown@10 下永远是 false,因为 v10 已经去掉了 inline prop,而且 node 是 HAST(type === 'element')不是 MDAST 的 'inlineCode'。所以段落中的行内 code 全都落到了 block分支,被渲染成 <SyntaxCodeBlock>(一个 <div>)放在 <p> 里。
    加了一条基于换行的兜底方案。markdown 规定 inline code 不能含换行,block code 的 children 一定带尾部 \n : const shouldInline = inlineDetected || !/[\r\n]/.test(raw); (commit a60a0cb)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants