Render before the brace closes
A model streams a UI spec token by token. Watch the partial-JSON repair, the progressive render, and a catalog guardrail reject a component that isn't allowed.
GoalUnderstand how declarative generative UI streams safely: repair the prefix, render what's known, validate against a catalog.
Every component and prop is in the catalog.
Press play. The model streams its UI spec ten characters at a time.
Nothing to draw yet.
| Node | Type | Status | Why |
|---|---|---|---|
| No nodes parsed yet. | |||
The catalog the renderer enforcesclick to expand
// catalog.ts — the only parts the model may use
const actions = z.enum(["approve_expense", "reject_expense", "request_info"])
export const catalog = defineCatalog({
Card: { props: z.object({ title: z.string().optional() }), children: true },
Text: { props: z.object({ text: z.string(),
variant: z.enum(["heading", "body", "muted"]).optional() }) },
Money: { props: z.object({ amount: z.number(),
currency: z.enum(["USD", "EUR", "CAD"]) }),
stream: "hold" }, // never show a half-arrived number
Badge: { props: z.object({ label: z.string(),
tone: z.enum(["ok", "warn", "fail"]).optional() }) },
Row: { props: z.object({}), children: true },
Button: { props: z.object({ label: z.string(), action: actions,
variant: z.enum(["primary", "default"]).optional() }) },
})
// Illustrative API, in the spirit of json-render and A2UI catalogs.How it works: repair, validate, renderclick to collapse
Repair the prefix. Close the open string, drop a dangling key or comma, then close every open bracket. Pane 2 shows exactly what was invented. It runs on every chunk, so it has to be cheap.
Pending is not invalid. Mid-stream, a missing prop or a half-typed enum is just late. Only refuse a value once it can no longer become valid, or once the stream is complete.
Hold what must be exact. A progressive amount reads $1, $12, $124 before $1,240. The Money component waits for its number to close. Decide per component what may flicker.
The catalog is the boundary. The model can ask for anything. The renderer draws only registered components with allowed props and actions, and refuses the rest visibly.
for await (const chunk of stream) {
text += chunk
const { value, complete } = parsePartialJson(text) // repair the prefix
render(value, { complete })
}
function render(node, ctx) {
const entry = catalog[node.type]
if (!entry) {
// "Car" may still become "Card"; "CreditCardForm" never will
return couldStillBecomeValid(node.type, ctx) ? <Pending/> : <Refused/>
}
const check = entry.props.safeParsePartial(node.props, ctx)
if (check.refused) return <Refused reason={check.reason}/>
return <entry.Component {...node.props}>
{node.children?.map(c => render(c, ctx))}
</entry.Component>
}