Build a generative UI conversation
A generative UI conversation answers with application components as well as text. Use it when a card, list, or other trusted view makes the response easier to understand and act on. The model chooses from exposed components and supplies their props; your application controls rendering and every action.
Let’s make a delivery assistant that can display a status card. The first implementation is read-only. Adding an Accept or Save button later requires a separate handler that checks completion, current data, and permission.
Rendering a proposal and accepting it are separate steps
Scroll horizontally to see the full diagram, or expand it.
Read the diagram
The application exposes trusted component contracts. Model output chooses one of those components and supplies schema-constrained props. Hashbrown resolves those props for the application-owned renderer. A read-only card displays information without changing application data. If a later card offers acceptance, a separate application handler checks completion, current data, and permission before applying a proposal. Rendering alone never accepts it.
Build a conversation with a trusted card
Start with the React Quickstart. The following frontend example uses its configured Hashbrown provider and same-origin /api/chat endpoint. Keep the provider key and model configuration on the server. Mount the component beneath HashbrownProvider and keep it mounted for the conversation lifetime.
import { s } from '@hashbrownai/core';
import { exposeComponent, useUiChat, useUiKit } from '@hashbrownai/react';
function DeliveryCard({ status }: { status: string }) {
return (
<article aria-label="Delivery status">
<h2>Delivery</h2>
<p>{status}</p>
</article>
);
}
const delivery = exposeComponent(DeliveryCard, {
name: 'DeliveryCard',
description:
'Display a read-only delivery status. Do not use for changing an order.',
props: { status: s.string('A short delivery status') },
children: false,
});
export function DeliveryAssistant() {
const kit = useUiKit({ components: [delivery] });
const chat = useUiChat({
system:
'Demonstrate the delivery card with this public example: the parcel is due Friday.',
components: [kit],
});
return (
<section>
<button
disabled={chat.isLoading}
onClick={() =>
chat.sendMessage({
role: 'user',
content: 'Show the delivery status.',
})
}
>
Show delivery
</button>
{chat.messages.map((message) =>
message.role === 'assistant' ? (
<div key={message.id}>{message.ui}</div>
) : null,
)}
{chat.error && <p role="alert">The reply could not finish.</p>}
</section>
);
}
The delivery date is public example data supplied in the instructions. For a real order, retrieve its status through an authorized read-only tool and keep the authoritative result in application state. The model can misreport a tool result, so a generated status card is a presentation, not a database record.
Send a follow-up through the same chat instance to retain the exchange. If a later card proposes a change, keep its draft separate from accepted state. Require a current completed response and an explicit application action; the next read tool should return what the application actually accepted.
Explore it in the workbench
The workbench below replays a recorded run of this lesson.
Propose a plan, explicitly accept it, and read accepted state in a read-only follow-up.
Recorded with real OpenAI requests, actual Hashbrown React kit and UI chat APIs, trusted framework components and the real readContext browser tool. Kit metadata and schema come from the actual framework kit. The server requires fresh context before UI generation, then disables further tool calls for that response. SummaryCard proposes a plan; PlanReading is a read-only model report. Only explicit validated acceptance changes application state. Unchanged protocol events are paced at 20 ms for filming; the normal host adds no delay. Footage is slowed to half speed for readability. Replay makes no requests or writes. No narration. Read the text walkthrough.
Let's give our conversation something to work with. The assistant will propose a two-step plan. We'll accept it, then ask the assistant to read the plan we accepted. One turn proposes a change; the next reports application state.
Follow both turns
- Open UI chatbot in the canonical workbench.
- Send the first prompt.
readContextreturns the current application plan and revision without changing them. - Review the generated
SummaryCard. Its appearance does not accept the proposal. - Select Accept plan after successful completion and validation. State now shows the accepted plan at revision one.
- Send the follow-up asking for the plan you accepted. Inspect the next tool result: it contains that actual plan and revision.
- Read the generated
PlanReading. It has no Accept action and leaves the revision unchanged.
Both React turns use real Hashbrown UI chat, browser tools, trusted component rendering, and OpenAI responses. The recorded model output is not supplied by a local fixture.
Keep the responsibilities clear
| Boundary | Responsibility |
|---|---|
| Model | Selects an exposed component and proposes its schema-constrained props. |
readContext | Returns the actual accepted plan and revision. |
SummaryCard | Presents a proposal with an explicit, guarded application action. |
| Accept handler | Validates the current completed response and advances application state once. |
PlanReading | Displays a read-only model report. |
| State inspector | Shows authoritative application values. |
A tool result gives the model context. It does not guarantee that the model will report every value correctly. Compare the reading with State when debugging. A read-only component must remain read-only even if a response arrives late or an old turn is selected.
Use one kit for the conversation
The checked React client packages SummaryCard and PlanReading with useUiKit() and passes the kit to useUiChat(). The component descriptions distinguish a proposed plan from a report of an accepted plan.
The composition point is small:
import { useUiKit, useUiChat } from '@hashbrownai/react';
const kit = useUiKit({ components: exposedComponents });
const chat = useUiChat({
system: instructions,
components: [kit],
tools: [readContext],
transport,
});
The complete client defines exposedComponents, the fixed instructions, readContext, and transport. Call the hooks at the top level of the React component.
The server fixes the instructions, output schema, and read-only tool catalog. It does not trust a browser-supplied system message to define that policy. Keep provider credentials on the server.
The application owns acceptance. The handler requires a current successful source, a valid original document, matching props and component, the current turn and revision, and an unused proposal. Replay, stopped or failed requests, stale cards, and PlanReading cannot apply a plan.
Inspect and replay
Use Requests to compare the two tool calls. Use Kit and Schema to inspect what the model can render. UI follows the selected component; State follows the accepted plan.
The bottom timeline keeps captured requests, JSON, components, and state together. Replay makes no provider requests or application writes. These local captures clear on refresh; this example does not save an agent checkpoint.
Require the context step
An instruction alone does not guarantee that a model will call a tool. In this example, the server requires readContext before allowing a UI response. Once a successful context result belongs to the current turn, it disables further tool calls for that response.
This is the example's server policy, implemented through the OpenAI adapter. It is not automatic behavior supplied by a kit or a chat hook.
Read the checked source
- Server-enforced context sequence.
- React conversation, trusted components, and lifecycle.
- Shared kit contracts and lesson policy.
- Capture and explicit acceptance.
Use UI Kits to package reusable components. Use Natural language forms when the user should edit generated fields before applying them. For saved text, continue with Message history.
