hashbrown

Generative UI with React components

Generative UI lets a model choose from components your application already owns. You describe each component and its inputs; Hashbrown turns the model’s structured response into those trusted components. The model supplies data and layout choices. Your code still owns rendering, events, and access to application services.

Use it when different requests need different presentations: a product comparison, a summary card, or a small dashboard. If every answer uses the same layout, structured output is often enough. Start with one useful component, then grow the catalog when the feature needs another way to present information.

The model selects; your components render

Scroll horizontally to see the full diagram, or expand it.

describeoutputresolvetrusted codeComponent catalogNames and prop schemasModelSelect from the catalogStructured UI treeComponent names and propsApplication rendererYour components and events
Expose a controlled catalog and prop schemas. Rendering and event handlers remain application code.
Read the diagram

The application catalog describes allowed components and their prop schemas to the model. Model output selects a component and supplies props. Hashbrown resolves that tree against the catalog, then the application renders its own component.

The model selects; your components render

Expose a controlled catalog and prop schemas. Rendering and event handlers remain application code.

Read the diagram

The application catalog describes allowed components and their prop schemas to the model. Model output selects a component and supplies props. Hashbrown resolves that tree against the catalog, then the application renders its own component.

Expose a useful summary card

Use the endpoint and client configuration from the Quickstart. Render the component below inside the existing HashbrownProvider.

This complete example defines the card, exposes its two text fields, and renders assistant output. The fields are text bindings, so the model cannot inject component code or HTML. No tool or application write is involved.

import { Fragment } from 'react';
import { s } from '@hashbrownai/core';
import { exposeComponent, useUiChat } from '@hashbrownai/react';

function BookCard({ title, summary }: { title: string; summary: string }) {
  return (
    <article>
      <h2>{title}</h2>
      <p>{summary}</p>
    </article>
  );
}

const components = [
  exposeComponent(BookCard, {
    name: 'BookCard',
    description: 'Summarize a book with its title and a short explanation.',
    props: {
      title: s.string('The book title'),
      summary: s.string('A short plain-text summary'),
    },
    children: false,
  }),
];

export function BookSummary() {
  const chat = useUiChat({
    system: 'Summarize books using one BookCard. Keep summaries concise.',
    components,
  });
  return (
    <main>
      <button
        type="button"
        disabled={chat.isLoading}
        onClick={() =>
          chat.sendMessage({
            role: 'user',
            content: 'Summarize Pride and Prejudice.',
          })
        }
      >
        Summarize
      </button>
      {chat.error && <p role="alert">The request failed.</p>}
      {chat.messages.map((message, index) =>
        message.role === 'assistant' ? (
          <Fragment key={index}>{message.ui}</Fragment>
        ) : null,
      )}
    </main>
  );
}

children: false keeps the catalog flat. Give each component a stable name and describe when it is useful. If a component performs a mutation, its event handler must check current state and permission independently of the generated props.

Explore it in the workbench

The workbench below replays a recorded run of this lesson.

Watch streamed JSON resolve into a trusted SummaryCard, then accept and revise the plan.

Recorded in the real React workbench with OpenAI requests, browser tools, Hashbrown streaming, and application state. Footage slowed to half speed for readability. Replay reads captured frames. No narration. Read the text walkthrough.

Open full workbench
Open full workbench

Let’s build a two-step plan. We will give the model one component, SummaryCard, and describe the props it accepts. As the response arrives, Hashbrown parses the JSON and renders our component.

I use the same plan in Tools and Structured output. Here, we’ll focus on how a model selects a component and supplies its props.

Run the same conversation

Use the panel above, or run the workbench on your machine.

The replay uses this checked React client with real provider requests. The local server keeps credentials, instructions, tools, and response schema on the server. Refresh clears the in-memory conversations and accepted plan.

Expose one trusted component

SummaryCard is ordinary application code: a title, an ordered list, and an Accept plan button. We expose that existing component with a stable name, a description, and a schema. The model chooses from this catalog; it does not write executable component code.

The following excerpt is from the checked workbench client. Open the complete client for the component implementation, application state, and acceptance guard.

react/src/Workbench.tsx
const components = [
  exposeComponent(SummaryCard, {
    name: 'SummaryCard',
    description:
      'Propose a concise plan for the user to review and explicitly accept.',
    props: {
      title: s.string('A concise plan title'),
      steps: s.streaming.array(
        'The ordered plan steps',
        s.string('A clear, concise action'),
      ),
    },
    children: false,
  }),
];

exposeComponent() connects the schema to the component’s props. A mismatched type is a build error. children: false disallows nested components; larger catalogs can allow children explicitly.

Watch the props arrive

The title uses an ordinary s.string. The steps use s.streaming.array, so resolved items can appear while the response is still arriving. Select JSON to follow the raw response, then UI to inspect resolved props. The Resolved props section follows that selection.

A partial value is useful feedback. It is still a preview. The workbench enables Accept plan only after a fresh, successfully completed response passes its two-step plan rules. Rendering the card does not update the accepted plan.

Start and render the conversation

The checked client passes the catalog, readContext tool, system instruction, and capturing transport to useUiChat(). The Quickstart shows the smaller tool-free setup; this lesson adds context and explicit acceptance.

Assistant messages expose message.ui, which React renders inside the conversation. Keep the complete message history for follow-up turns.

Read the checked client. The workbench shell records requests and parsed output; React owns the live component renderer.

Follow the second turn

Send the first prompt and select Accept plan. In State, the application revision advances from 0 to 1. Then choose Follow-up and send “Make the second step shorter.” The tool reads the accepted plan before the model proposes its revision.

Use the bottom player to revisit the first streamed JSON, parsed output, or acceptance. The chat, inspector, and state follow the same captured moment. Raw events puts every frame on the scrubber; playback makes no provider requests and cannot accept a plan.

Grow the component catalog

Use descriptions that explain when a component is useful and what its props mean. Keep names stable. If the layout accepts children, use children: 'any' for any exposed child, or provide an explicit collection of exposed components. Those children still come from your trusted catalog.

Skillet also accepts its supported Standard JSON Schema inputs. Read Skillet schemas before substituting another schema library; unsupported constructs are rejected.

Next steps

Continue with Tools to inspect readContext, or Structured output to return the same plan as typed data. See Streaming for string, array, and object streaming, and the useUiChat() API for the complete options.

Generative UI with React componentsExpose a useful summary cardExplore it in the workbenchRun the same conversationExpose one trusted componentWatch the props arriveStart and render the conversationFollow the second turnGrow the component catalogNext steps