How the model produces charts, tables, and metric cards.
A reply can carry a rendered widget when a chart, table or card says it better than prose. The model emits the data; Breeze does the rendering.
The model emits data, never JSX
Every spec is zod-validated against a closed, compile-time whitelist in
lib/genui/schema.ts. Component dispatch is an exhaustive switch, not a lookup
keyed on a model-supplied string, and an unknown type renders as collapsed
JSON. Nothing from the model is ever evaluated. See
Security.
An assistant reply embeds a fenced code block:
```breeze-ui
{ "type": "chart", "title": "Revenue", "variant": "bar",
"data": [{ "name": "Q1", "value": 84 }, { "name": "Q2", "value": 96 }] }
```The fence rides inside the reply's normal text, which is the whole trick: it needs no new stream event, no API change and no database field. It persists with the message and re-renders on reload for free.
The grammar is a closed whitelist. tabs is the only one that nests -- it holds
leaf widgets, not other tabs.
Prop
Type
Chart colours are not free-form
They come from a validated fixed palette, documented in tasks/chart-design.md
and re-checkable with scripts/validate_palette.js. A model-supplied colour is
not a thing the grammar can express.
The request carries a genui field.
The default. The backend asks a router model whether this turn warrants UI. It answers yes only for quantitative data -- charts, tables, metric cards and comparisons. Prose, factual questions and code get a plain reply from the normal local path.
Generative UI composes with web search: "chart the last 10 days of weather in Hyderabad" searches and renders a chart in one turn. Search results reach the answer as evidence in the user turn rather than as tool messages, so the widget model never needs to call a tool. See Web search.
Both compete for the same context window, so on a combined turn history is trimmed harder and the evidence block is capped tighter.
backend/genui_prompt.py teaches the same rules lib/genui/schema.ts enforces.
metrics tile, never a one-bar chart.Keep the prompt and the schema in lockstep
backend/genui_prompt.py and lib/genui/schema.ts are two statements of one
grammar. Changing the zod schema without changing the prompt means the model
keeps emitting specs the validator now rejects.
Read this before editing backend/genui_prompt.py
Ollama's default context window is 4096 tokens, and its OpenAI-compatible
endpoint silently ignores options.num_ctx -- verified, not assumed.
When the prompt overflows, Ollama truncates from the front, which evicts the system prompt. The model loses both the widget grammar and its Breeze identity, and answers "I'm unable to display charts". Three consequences, all of which are budget decisions rather than preferences:
Held to roughly 650 tokens, enforced by genui_prompt.test_prompt_budget(). If
you add a widget type, something else has to come out.
max_tokens is capped at 1536Prompt and completion share the one window. Asking for the full 4096 back guarantees the front of the prompt is evicted to make room.
_trim_history runs so that a long conversation cannot push the grammar out of
the window.
Raising OLLAMA_CONTEXT_LENGTH on the Ollama server relaxes all three, but the
defaults have to work unconfigured.
/dev/genui is a fixture harness that renders every widget in
components/genui/ against sample specs, with no model in the loop. It is the
fastest way to check a rendering change.