App builder
Describe an app and watch it build: chat on one side, the running app on the other with versions, device sizes, a code view that streams files as they are written, and select and comment tools over the preview.
On this page
The demo plays a scripted stream, so it needs no API key. The agent builds an orders dashboard, then adds a revenue chart and a theme toggle. The workspace shows the code while files are written and switches to the preview when each version lands. Afterwards, step between versions, try the device sizes, or pick an element with Select. Open it full screen.
Installation
The block installs to components/chatcn/app-builder/app-builder.tsx with every component it composes. It is your code from here: swap the header, add a deploy button, change how prompts are worded.
Usage
Pass the useChatcnChat result as chat. The workspace comes from the message stream: file write and edit tool calls build up the files, and a data-app-version part at the end of each build marks a version with the HTML to preview.
"use client"
export default function Page() {
const chat = useChatcnChat({
transport: new DefaultChatTransport({ api: "/api/build" }),
})
return (
<AppBuilder
chat={chat}
title="Northside orders"
fullHeight
suggestions={["An orders dashboard for a coffee roastery", "A booking page for a barber"]}
onPublish={() => publish()}
/>
)
}On the server, stream the version part once the build finishes. srcDoc is self-contained HTML; pass url instead to load a dev server or a preview deployment.
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({ model, messages, tools: { write_file, edit_file } })
writer.merge(result.toUIMessageStream())
await result.consumeStream()
const html = await buildPreview(sandbox)
writer.write({
type: "data-app-version",
id: crypto.randomUUID(),
data: { label: "Revenue chart", srcDoc: html, createdAt: Date.now() },
})
},
})
return createUIMessageStreamResponse({ stream })Versions and views
Versions and the view work controlled or uncontrolled. Uncontrolled, the switcher follows each new version while you are on the newest one, and the view follows the agent: code while it writes, the preview when a version lands, until you pick a view yourself. Pass onRestoreVersion to show Restore on older versions. To keep versions in your own store, pass versions and the block stops reading them from messages.
const [version, setVersion] = useState<string>()
<AppBuilder
chat={chat}
version={version}
onVersionChange={setVersion}
onRestoreVersion={(id) => chat.actions.send?.(`Restore version ${id}`)}
/>Select and comment
The preview runs in a sandboxed frame. For srcDoc versions the block adds a small inspector script, so Select and Comment work without reaching into the frame. Mark the parts worth picking with data-inspect="Revenue chart". A picked element becomes a chip in the composer, and formatPrompt turns the text and the element into the prompt (by default Revenue chart: make the bars thinner). Turn it off with visualEdit={false}.
Helpers are exported for your own layouts: appBuilderTimeline(messages) returns the versions, the current files, the paths changed since the last version and the file being written; appBuilderVersions, appBuilderFiles and withInspector cover the common cases.
Components it composes
| Component | Role |
|---|---|
| Conversation | The thread, with stick to bottom scrolling |
| Message, Message parts | Turns, reasoning, tool calls and the version rows |
| Markdown | Agent replies |
| Reasoning | The plan before the build |
| Write file, Edit file, Bash | File writes, edits and the build command in the thread |
| Prompt input | The composer, with Stop while building |
| Context chips | The element picked in the preview |
| Web preview | The running app: address, device sizes, reload, console |
| Preview toolbar | Select and comment over the preview |
| Artifact versions | Step between versions, with line changes |
| File tree | Generated files with added and modified markers |
| Code block | The open file, streaming while it is written |
| Resizable | The split between chat and workspace |
| Empty state, Suggestions | The first screen and its starters |
Responsive
The block reads its own width with container queries, so it works in a sidebar as well as full screen. From 48rem it is two resizable panes. Below that it shows one pane at a time, with Chat, Preview and Code tabs under the header (44 px targets on touch). Picking an element on a phone switches back to Chat with the chip in place. The generated files show as a tree beside the code when there is room, and as a row of tabs above it when not.
Props
| Prop | Type | Notes |
|---|---|---|
chat | useChatcnChat() result | Messages, status and actions in one prop |
messages, status, actions | Override chat, or use any runtime | |
title | ReactNode | Project name in the header |
onPublish, publishLabel | () => void, ReactNode | Shows Publish once there is a version |
headerActions | ReactNode | Extra controls before Publish |
versions | AppBuilderVersion[] | Defaults to the data-app-version parts in messages |
version, defaultVersion, onVersionChange | string | Controlled or uncontrolled |
onRestoreVersion | (id) => void | Shows Restore while an older version is open |
view, defaultView, onViewChange | "preview" | "code" | Uncontrolled, follows the agent |
device, defaultDevice, onDeviceChange | "desktop" | "tablet" | "mobile" | Passed to the web preview |
previewUrl | string | Address shown for srcDoc versions. Defaults to localhost:5173 |
previewError | string | { title, message } | Shows the error view in the preview |
building | boolean | Override the building state |
visualEdit | boolean | Select and comment tools. Default true |
formatPrompt | (text, element) => string | Words the prompt sent for a picked element |
emptyTitle, emptyDescription, suggestions | The first screen | |
placeholder | string | Composer placeholder |
toolRenderers | ToolRendererMap | Defaults to file, search and command renderers |
fullHeight | boolean | Fill the viewport instead of the parent |
Keyboard
| Key | Action |
|---|---|
Enter | Send. Shift+Enter adds a line, and nothing sends while an IME is composing |
Escape | Stop the build while the composer has focus. Clears the preview tool |
S, C | Select or comment in the preview, outside text fields |
ArrowLeft, ArrowRight | Move between Preview and Code, or the narrow tabs |
| Arrow keys on the divider | Resize the panes. Shift moves ten times as far |
Accessibility
- Preview and Code, and the narrow pane switcher, are tab lists with
aria-selected. - The thread is a focusable
log; finished replies are announced once, never per token. - Building v2 is announced politely when a build starts. Version rows are buttons with
aria-pressedfor the one showing. - The preview frame is titled with its version, for example
Preview, v2. - File changes never rely on color alone: the tree shows A and M letters and line counts.
Motion
- The view and tab pills slide in 200 ms on pointer input and jump on keyboard input.
- The building pill rises in over 200 ms; files in the first build enter one by one as they are written.
- Device changes settle the frame with a short scale and fade, never a width tween.
- Under reduced motion only opacity changes remain.