Dialogue · Design notes · LLM whiteboards
← Design notes

Can a persona chart & draw on a whiteboard?

A persona in the room outputs text. The question is what text it can emit that arrives as a diagram or a drawing rather than prose. This note runs the three candidates you named — Mermaid, Excalidraw, tldraw — as live, working demos on this page, each paired with the exact thing an LLM would have to write to produce it. Then it picks the one that fits the room as it is today, and parks the one that fits where the room is going.

Mermaid shipped — diagrams now render in-room (a ```mermaid fence → the wildcard pane; vendored mermaid + panzoom). See the Scratch-paper interaction demo. The shared canvas (tldraw/Excalidraw) below remains a parked phase-2 idea.
The deciding axis isn't "which library is prettiest." It's what the model has to emit. One of these is just text in a code fence — it drops into our existing render pipeline next to KaTeX and syntax highlighting with no architecture change. The other two are full React canvas editors driven by a scene object or an API call — a different surface, a heavier build, and a different place in the product.

Two families, not three

Before the demos, the split that matters:

① Mermaid — text in a fence

what the LLM emits → a fenced mermaid block

The lightest by a wide margin: one small library, no React, renders to SVG in the browser. A persona writes the diagram the same way it writes a code block. Here is a live render of the room's own turn-routing, drawn entirely from the text below it:

rendering…

the model output that produced the SVG above — nothing else:

```mermaid flowchart TD U[Human message] --> G{Room busy?} G -- idle --> A[Answer now] G -- busy --> Q[Queue] Q --> D[Drain queue in one turn] A --> P([Panel turn]) D --> P P --> S[(SSE to every client)] ```

Mermaid also covers sequence diagrams, state machines, class diagrams, ER diagrams, Gantt charts, pie charts and mind-maps from the same fence — the model just changes the first keyword. Because it is pure text, it streams, it diffs, and it survives copy-paste. The cost: it is a fixed grammar — boxes, arrows, lanes. You cannot ask it to "sketch a face" or place a free-form annotation.

Making it readable — mobile-first

Out of the box Mermaid renders the SVG at its intrinsic pixel size and pins an inline max-width on it, so a small diagram sits marooned in a wide frame with ant-sized text. We're phone-first, so the fix has two halves — fluid sizing, and a layout that suits a portrait column:

LeverWhat & why
Make the SVG fluidStrip Mermaid's width/height + inline max-width and let the viewBox drive it (width:100%; height:auto). It then scales up to fill the column and the text grows with it. Mermaid config: flowchart:{ useMaxWidth:true } gives the width:100%; removing the cap lets it grow past intrinsic size.
Go vertical (TD/TB)The biggest mobile lever. A wide LR chart squeezes many nodes across a 375px screen → tiny text. TD stacks them down a narrow column — fewer nodes per row, bigger glyphs. The demo above switched LR → TD for exactly this.
Cap the reading columnFull-bleed on a phone; centered and capped (~540px) on desktop, so a simple diagram scales to a comfortable size rather than a giant one.
Bump the base fontSet themeVariables.fontSize to 16px and keep node labels short — long labels are what force the auto-shrink in the first place.
Escape hatch for dense chartsSome diagrams genuinely can't shrink to a phone. Don't fight it — wrap the SVG in pan/zoom (svg-pan-zoom) or a tap-to-expand lightbox instead of rendering ants.

the few lines that did it on this page:

mermaid.initialize({ theme:'base', themeVariables:{ fontSize:'16px' }, flowchart:{ useMaxWidth:true } }); // after render — let the viewBox drive a fluid SVG: const el = host.querySelector('svg'); el.removeAttribute('width'); el.removeAttribute('height'); el.style.width = '100%'; el.style.height = 'auto'; el.style.maxWidth = '100%'; // CSS: #mmd { max-width:540px; margin:0 auto } — phone-full, desktop-capped
drop-inFor our app this is a stage-2/3 renderer, exactly like the KaTeX / highlight.js plan in Rendering & speed: detect a mermaid fence in a bubble, hand the body to mermaid.render(), apply the fluid-SVG fix, swap it in. One pinned CDN script, plus one prompt line that tells diagram-inclined hosts the fence exists and to prefer TD over LR — the bubble is a narrow phone column, so vertical is the right default.

The scratch-paper interaction — a clickable phone mock: the persona says his line in the bubble and the mermaid lands on the wildcard pane; tap to bring the board up. Live Mermaid inside.

② Excalidraw — a hand-drawn scene

what the LLM emits → an element skeleton (JSON)

Excalidraw is the sketchy, whiteboard-marker aesthetic. The model doesn't write a DSL — it emits a skeleton array of elements (rectangles, ellipses, arrows, text, with ids so arrows can bind), which convertToExcalidrawElements() expands into a full scene the editor mounts. It is a real editor: pan, zoom, grab a shape and move it.

Loads React 18 + the Excalidraw editor (~a few MB) from the esm.sh CDN. Heavier than Mermaid by orders of magnitude — and slow / unreachable behind the GFW. Click to mount the editor and render the LLM-authored scene below.

the skeleton an LLM would emit (rendered above once activated):

convertToExcalidrawElements([ { type:'rectangle', id:'persona', x:40, y:60, width:200, height:90, backgroundColor:'#f2dfd6', strokeColor:'#cc785c', label:{ text:'AI persona' } }, { type:'rectangle', id:'human', x:40, y:230, width:200, height:90, backgroundColor:'#dde3ea', strokeColor:'#5f7186', label:{ text:'Human' } }, { type:'ellipse', id:'board', x:430, y:120, width:240, height:140, backgroundColor:'#dbe7dc', strokeColor:'#5e8b6e', label:{ text:'Shared whiteboard' } }, { type:'arrow', x:250, y:100, width:170, height:40, start:{ id:'persona' }, end:{ id:'board' }, label:{ text:'draws' } }, { type:'arrow', x:250, y:280, width:170, height:-40, start:{ id:'human' }, end:{ id:'board' }, label:{ text:'edits' } }, ])

Strengths: genuinely free-form (arbitrary positions, the hand-drawn look, embedded text), an editable result a human can keep working on, and a clean export to PNG/SVG. Costs: it is a React app you mount — not a string in a bubble — and the model has to emit valid, positioned JSON, which is wordier and more error-prone than a DSL.

③ tldraw — shapes via an editor API

what the LLM emits → calls to editor.createShapes([...])

tldraw is the most capable canvas of the three and the only one built around real-time multiplayer. You don't hand it a static scene; you mount the editor and, on its onMount, drive it through an API — createShapes(), createBindings(), zoomToFit(). The "LLM output" is therefore a sequence of shape records the host code passes to that API.

Loads React 18 + the tldraw SDK from esm.sh. Same weight caveat as Excalidraw — large, and slow / blocked behind the GFW. Click to mount the editor; the shapes below are drawn programmatically on mount.

the shape records an LLM would produce, fed to the editor on mount:

editor.createShapes([ { type:'geo', x:80, y:80, props:{ geo:'rectangle', w:200, h:90, color:'orange', fill:'solid', richText: toRichText('AI persona') } }, { type:'geo', x:80, y:240, props:{ geo:'rectangle', w:200, h:90, color:'blue', fill:'solid', richText: toRichText('Human') } }, { type:'geo', x:430, y:150, props:{ geo:'ellipse', w:240, h:140, color:'green', fill:'solid', richText: toRichText('Shared whiteboard') } }, { type:'text', x:430, y:320, props:{ richText: toRichText('drawn by createShapes()') } }, ]) editor.zoomToFit()

Strengths: the richest shapes, arrow bindings, frames, a polished editor, and a sync store for collaborative multi-cursor canvases — which maps directly onto our multi-user, SSE model. Costs: the heaviest bundle, the steepest API, and licensing to read before any production use (tldraw's watermark/license terms).

Side by side

LibraryWhat the model emitsRenders viaWeightStreams?Free-form?
Mermaida mermaid text block (DSL)client → SVG, no React tiny✓ it's text✗ fixed grammar
Excalidrawan element skeleton (JSON)mounted React editor heavy✗ needs whole scene✓ hand-drawn
tldrawcreateShapes() recordsmounted React editor heaviest✗ API-driven✓ + multiplayer

Recommendation for the room

shippedMermaid shipped as a bubble renderer. It was the only one of the three that fits a chat app where the unit of output is a text turn. It cost one include and one detector in the render pipeline, gives every persona flowcharts, sequence diagrams and charts for free, and — being text — it streams and diffs like the rest of a turn. In the live room a ```mermaid fence lifts out of the speech and renders on the wildcard pane (vendored mermaid + panzoom). It was a small PR, not a new surface.
laterA shared canvas is a separate product surface, and tldraw is the one to reach for. If we want a persona and the humans to draw together on a persistent board, that's a dedicated "Board" panel mounted beside the chat — not an inline bubble. tldraw's multiplayer sync store is the natural fit for our multi-user model; Excalidraw is the lighter pick if we only need single-author sketches with no live collaboration. Either way it's a phase-2 build, gated on a real use-case, with its bundle weight and license read first.

In one line: Mermaid is a renderer we add to the pipeline; a whiteboard is a surface we'd build. Start with the renderer.

Sources

Mermaid — docs & live editor · Mermaid render API
Excalidraw — browser integration (ESM CDN) · Element skeleton API
tldraw — quick start · Controlling the canvas (createShapes) · tldraw license

Dialogue · research note · 2026-06-12 · live demos load Mermaid (jsDelivr) and React + Excalidraw/tldraw (esm.sh) from public CDNs — see also Rendering & speed