Day 9

Generative UI

You ship
An AI dashboard generator with a catalog of 15 or more components, and 3 demo scenarios.

What you'll build today

After Day 8, WorkBot answers in a web chat, but every answer is text. An operations lead who asks how the open requests split by team is better served by a chart and a table than by a paragraph.

Today you build generative UI, where the model composes a page from React components instead of writing prose. You use json-render, Vercel's framework for constrained UI generation. The model gets a catalog, a fixed list of components with a schema for each one's props, so it can only build pages from parts you approved. You learn how a page streams as JSON, how the browser draws it while it arrives, and how the page keeps its own state.

By the end of the day you can describe a dashboard, a form or a timeline and watch WorkBot build it from the company's live numbers. A follow-up description edits the page on screen, and a generated form files a real request.

By the end of the day you can

  • Build AI interfaces that render React components the model chooses
  • Design safe component catalogs with Zod schemas
  • Render the page progressively as the model streams its JSON

Topics

Generative UI with json-render

  • What generative UI is: the model outputs structured JSON that maps to React components, and the page renders them as they arrive
  • Why it matters: the model can generate dashboards, forms, charts and cards, not only text
  • json-render: Vercel's framework for constrained UI generation
  • The 3-step pattern: define a catalog, let the model generate JSON, and render it progressively

Designing Component Catalogs

  • What a catalog is: the set of components the model is allowed to generate
  • Defining a component: a name, a props schema (Zod) and a render function
  • Safety: the model can use only the components you define, and never arbitrary HTML or JavaScript
  • Schema design: make props specific enough to be useful and flexible enough for variety
  • Built-in components: json-render ships with 39 components (buttons, forms, charts, tables and more)
  • Custom components: add your own React components to the catalog
  • Actions: 6 built-in action types for interactivity (submit, navigate, toggle and more)

Progressive Rendering

  • Streaming JSON: json-render renders the interface as the JSON tokens arrive
  • Partial rendering: components appear and fill in as the stream goes on
  • Loading states: a skeleton interface while components are still being generated
  • Error handling: what to show when the model generates invalid JSON

Dynamic Layouts

  • Charts: bar, line and pie, generated from data the model analyzes
  • Data tables: sortable, filterable tables with columns chosen at run time
  • Forms: multi-step forms with validation and conditional fields
  • Cards and grids: product cards, user profiles and dashboard widgets
  • Interactive elements: buttons that trigger actions, expandable sections and tabs

Build

Features shipped today

  • WorkBot: Pages generated from a description, from a catalog of components: an operations dashboard on the live requests, a form that files a request, and a timeline of requests

Build an AI-powered dashboard generator:

  1. Define a component catalog of 15 or more components: charts (bar, line, pie), data tables, stat cards, forms with validation, progress bars, alerts, navigation tabs and image galleries
  2. Build the generation endpoint: take a natural language prompt ("show me a sales dashboard for Q4") and generate a full page layout through json-render
  3. Implement progressive rendering: components appear and fill in as the stream arrives
  4. Add interactivity: form submissions trigger new generations, charts drill down, and tabs switch
  5. Build 3 demo scenarios:
    • A sales dashboard with revenue charts, a top products table and KPI (key performance indicator) cards
    • A user onboarding form that adapts to earlier answers
    • A data exploration tool that generates charts from questions asked in plain language

Step by step

Define the component catalog

The catalog is what keeps generated pages safe, so it comes first. The model composes from this fixed list of components, each with a schema for its props. A generated page can then render only components you approved, with props the schema accepts.

Study first

  • Learn json-render's spec from its introduction. A spec is a root, a map of elements and optional state, and each element has a type, props and children.
  • Read the streaming guide, which shows how the model streams the spec as JSON patches, small edits that each set one part of it. The patches follow JSON Patch, and useUIStream applies them for you.
  • Read the catalog guide for defineCatalog, which takes components with Zod props, slots for children and a description the model reads. catalog.prompt() turns the catalog into the model's instructions.

Build

  1. Run pnpm add @json-render/core @json-render/react and pnpm dlx shadcn@latest add alert progress tabs.
  2. Write src/lib/catalog.ts with 22 components. Page, Stack, Grid and Card handle layout, and Heading and Text hold text. Metric and Progress show numbers, and Badge and Alert show labels.
  3. Add the data components Table, BarChart, LineChart, PieChart and Timeline, and Tabs. Then add a form made of Form, Input, Textarea, Select, Checkbox and Button, with one action, submit_request.
  4. Write descriptions that say when to use each component, and keep the props narrow, because the model reads both. Badges and alerts take a tone enum, and a grid takes 2 to 4 columns.
  5. Write a test that the catalog has at least 15 components, and that catalog.prompt() names the main ones and the submit_request action.

Done when

  • pnpm test passes.
  • When you print catalog.prompt(), it lists the 22 components with their props.
Technical notes
  • Make props nullable rather than optional, so the model sends every prop, with null where there is nothing.
  • A Metric takes a string or a number as its value, so "$1,240" and 17 both render.
  • The catalog is the contract between the prompt and the renderer. defineRegistry is typed by the catalog, so tsc fails when a component is added to one without the other.

Stream a page from a route

With the catalog defined, one model call needs the request, the company's data and the catalog's instructions together. The route sends all three and streams the model's JSON straight through to the browser.

Study first

  • Learn streamText with createTextStreamResponse and toTextStream, which together return a plain text stream, the format useUIStream reads. The AI SDK documents that format as its text stream protocol.
  • Learn the prompt's 3 parts. They are the catalog's own prompt, the data as JSON, and the page on screen when the request is an edit. The AI SDK guide in json-render's docs shows a standalone route that builds its instructions from catalog.prompt() and the current page.

Build

  1. Write requestSummary() in src/lib/requests.ts. It runs one grouped query and returns { total, byStatus, byTeam, byPriority, latest }, where latest holds the 10 latest requests.
  2. Write src/app/api/generate/route.ts, and validate { prompt, currentSpec? }, where the spec is json-render's { root, elements, state }.
  3. Put the summary in the instructions with "use these numbers as they are and never invent figures". For an edit, include the current spec, and then stream the text back.
  4. Write a unit test with a mock model that emits 2 patches, and check the stream and the instructions.
  5. Write a live test on claude-haiku-5-5, run by pnpm test:live. It creates a request, asks for a dashboard, and finds the root, a chart or table, and that request's title in the stream.

Done when

  • curl -N against /api/generate with a prompt prints JSON patches, and the first one sets /root.
Technical notes
  • Put the data in the instructions, not in the user message, so an edit request keeps the same numbers in view.
  • currentSpec is the page on screen. With it, "make the chart a pie" changes the chart and keeps the rest, because the model edits the spec instead of starting over.
  • A text stream carries no error events. When a reply has no root at all, the page says "No page came back" instead of showing a blank.

Render the page as it streams

The patches arrive one at a time, so the browser has to draw a page that stays incomplete until the stream ends. useUIStream turns the patches into a spec, Renderer draws whatever exists so far, and the page keeps its state in a store it owns.

Study first

  • Learn useUIStream, which returns spec, isStreaming, error and send(prompt, { previousSpec }).
  • Read the registry guide for defineRegistry(catalog, { components, actions }). It takes one React component per catalog entry, typed by the catalog's props, along with the action handlers.
  • Learn JSONUIProvider with a controlled store, and useBoundProp for a prop that is bound to state, such as the open tab or a form field.

Build

  1. Write the registry in src/components/generated-ui.tsx, building each component from the shadcn primitives. Draw the charts with small SVG and CSS rather than a chart library.
  2. Write GeneratedUI, which creates a state store once. An effect copies the spec's state into the store whenever the spec changes, so state that streams in after the element that reads it still shows up.
  3. Write src/components/generator.tsx, with a Textarea, 3 example prompts and the generated page below them. The examples are the curriculum's 3 scenarios as WorkBot's: an operations dashboard, an intake form and a timeline. In the reference run, the generator sits on its own page, /dashboards.

Done when

  • The operations dashboard example renders its counts as the JSON streams.
A generated dashboard
In the operations dashboard example, the model laid out counts and charts from the live requests, using the catalog.
Technical notes
  • Elements can arrive before the state they bind to. The json-render library wraps each element in an error boundary that resets only when the element itself changes. A component that throws on an undefined list therefore stays hidden after the state arrives, so default every list prop to [].
  • The provider's uncontrolled store misses a patch that changes an array in place, because the array's identity is unchanged. Owning the store, and copying a clone of the state on each spec change, avoids that comparison.
  • Tabs is the shadcn component in controlled mode. useBoundProp holds its value in the spec's state, so a visible condition on another element can read it.
  • The catalog has no skeleton placeholders, sortable tables or chart drill-downs. When a page needs one, add the component with its renderer and a test.

Edit pages and file requests

A generated page becomes more useful when you can refine it and act on it. A follow-up description sends the current spec back, so the model edits the page instead of starting over, and a form's button runs an action that files a real request.

Study first

  • Learn how send(prompt, { previousSpec }) sends the page on screen along with a follow-up description.
  • Read the actions section of the registry guide. An action is a named handler that a component's event, such as a button press, runs.

Build

  1. In the generator, send each follow-up description with the current spec.
  2. Write the submit_request action. It parses the form's values with the request schema and posts them to /api/requests, then writes the outcome to state for the page to show.

Done when

  • The form example files a request that appears on /requests.
  • A follow-up description edits the page on screen.
A generated form
In the form example, the fields come from the catalog and are bound to state, and the button's action files the request.
Technical notes
  • Action handlers need the store, which exists only inside the provider. A small Actions component builds them with useStateStore and hands them to ActionProvider.

Resources