Skip to content

feat(expert): render mermaid diagrams in messages - #9011

Open
andypalmi wants to merge 10 commits into
mainfrom
feat/expert-mermaid-diagrams
Open

andypalmi wants to merge 10 commits into
mainfrom
feat/expert-mermaid-diagrams

Conversation

@andypalmi

@andypalmi andypalmi commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #9010
Closes FlowFuse/engineering#577
Closes FlowFuse/engineering#578

  1. Mermaid blocks in Expert messages are rendered as diagrams. The diagrams are generated in the backend and sent with the message, so the frontend ships no diagram library.
  2. Each diagram shows as soon as its closing fence has streamed in, so the first diagram of a message doesn't wait for the rest of the message.
  3. The diagram is styled with the FlowFuse theme colours and the SVG is sanitized before it is shown. It is sanitized once per message, not on every streaming tick, and its ids are removed because they would repeat across the diagrams on a page.
  4. The copy button copies the Mermaid source. A Code toggle switches between the diagram and the source.
  5. A diagram can be copied as SVG, copied as an image, or downloaded as a PNG. These buttons only appear when a diagram renders. Exports carry the theme colours on screen, so a diagram exported in dark mode stays dark. The PNG download link is released after a delay, as releasing it straight after the click can cancel the download in Safari and Firefox.
  6. Blocks without a rendered diagram keep showing the source.
  7. Mermaid code is syntax highlighted.
  8. Expert text streams about 12x faster.
  9. Streaming keeps the whitespace of the message. It used to collapse it, so code blocks lost their indentation and copying a streamed block gave flattened code, which breaks indentation-based diagrams such as mindmaps.

Open question

  1. The chat is kept in sessionStorage, which browsers cap at about 5MB per site, and each message now also stores the SVG of its diagrams (usually 2 to 10KB each). A typical chat stays far below the cap, but a very long one with many diagrams could reach it, and messages after that are no longer saved across a refresh. Should we leave this as is, keep only the diagram source and show reloaded diagrams as code, or move the chat to IndexedDB?

Testing

  1. Unit tests for the markup and the mermaid highlighting.
  2. Unit tests for the diagram rendering, sanitizing, theme on export, the PNG download and the whitespace kept while streaming.
  3. Manual check in the Expert chat against Expert dev.

Mermaid code blocks render as diagrams once streaming completes, with a copy button for the source and a toggle between the diagram and the source.
@andypalmi andypalmi self-assigned this Oct 21, 2026
@andypalmi
andypalmi requested a review from Steve-Mcl October 21, 2026 10:20
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.54%. Comparing base (3ef2877) to head (01f1246).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9011      +/-   ##
==========================================
+ Coverage   79.46%   79.54%   +0.07%     
==========================================
  Files         475      475              
  Lines       26028    26076      +48     
  Branches     6938     6950      +12     
==========================================
+ Hits        20684    20741      +57     
+ Misses       5344     5335       -9     
Flag Coverage Δ
backend 79.54% <ø> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… speed up streaming

Add copy as SVG, copy as image and download PNG to rendered mermaid blocks, using the shared ff-btn styles. Diagrams render as soon as their closing fence has streamed in, and text streams several words per tick.
@andypalmi
andypalmi marked this pull request as draft October 21, 2026 10:52
…d of bundling mermaid

The Expert now sends each rendered diagram's SVG alongside the message, so the
frontend no longer needs the mermaid dependency. The SVG is sanitized, matched
to its code block by source, and recoloured with the FlowFuse theme variables.
… whitespace

Streaming collapses runs of whitespace in code, so the indented source of a block no longer equalled the source the diagram was rendered from.
The diagrams take their colours from variables, so one set of values follows the FlowFuse theme instead of overriding the diagram's own classes.
… highlight mermaid code

Mermaid blocks are now a plain code block variant. The Code toggle and export buttons are added once the diagram is shown and the toggle uses the hidden attribute, which removes the state-driven CSS and leaves other code blocks as they were. Mermaid sources get a highlight.js grammar.
@andypalmi
andypalmi marked this pull request as ready for review October 21, 2026 16:49
@andypalmi
andypalmi requested a review from cstns October 21, 2026 16:49
andypalmi added 2 commits October 23, 2026 16:03
Streaming split the message on whitespace and joined the words with single spaces, so code blocks kept collapsed indentation after streaming, and copying a streamed block gave flattened code. Each word now keeps the whitespace before it, so the streamed text ends identical to the message.
…n the page theme

Diagrams are sanitized once per message instead of on every streaming tick, and the copy and export buttons use the sanitized SVG. Ids are removed from diagrams because nothing in them refers to the ids and they repeat across the diagrams on a page. Copy SVG, Copy image and Download PNG write the theme colours of the page onto the SVG, so exports match the diagram on screen in light and dark mode. The PNG download URL is revoked after a delay, as revoking it straight after the click can cancel the download in Safari and Firefox.

@n-lark n-lark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey from local testing with mocked mermaid responses this does seem to work, but I couldn't get my local setup connected to Expert dev, so I haven't tested it end to end. Worth having someone else run it against dev before it merges.

That said, the approach gives me some pause. Most of the frontend doesn't render features with vanilla DOM manipulation. Here, though:

  1. Diagrams and buttons are added by hand after v-html renders, using appendChild, innerHTML, setAttribute, querySelector, and buttons built as HTML strings.
  2. Finished blocks are found by searching the streamed HTML for the class name and </pre>.
  3. It depends on the class names from Markdown.js staying exactly the same.

Could we potentially split the content with markedInstance.lexer()? Each mermaid block could then be its own MermaidDiagram component that just renders the SVG from the backend, no mermaid library needed. It would only be built once, state could live in refs, and we'd get ff-button / ff-kebab-menu.

Extending the current pattern would be a concern. Each new interactive piece would add more class-name coupling and more state that gets wiped on every rebuild, and it's a bit of an outlier in the codebase.

const svg = this.findDiagramSvg(source)
if (!svg) return
const theme = this.diagramTheme(block)
const label = btn.textContent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So clicking twice within 2s saves "Copied" as the label, and the button stays that way until the page is reloaded.

Image

Could do CODE_BLOCK_ICONS.ICON_CHECK swap that the code-block copy button already uses next to each label. That's consistent with it and avoids this bug.

this.renderDiagrams()
},
updated () {
this.renderDiagrams()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So while streaming, :key="text" rebuilds the whole content every tick and updated() re-injects the diagrams each time, so the diagram buttons don't work until streaming ends. None of my clicks on Code / export registered.

Could each diagram be its own Vue component instead of going through v-html? Then it wouldn't rebuild every tick, and we could use ff-button / ff-kebab-menu, which would also fix the export buttons wrapping in the drawer on smaller screens.

This branch was successfully deployed

1 active deployment
staging — 01f1246a Deployed Oct 23, 2026 by andypalmi via Deploy application #12700
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.

Render Mermaid diagrams in Expert messages

2 participants