Skip to content
BlocksApp builderMenu

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.

Describe an app, watch it build, then switch between preview, code and versions.

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

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/app-builder.json

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.

app/page.tsx
"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.

app/api/build/route.ts
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.

tsx
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

ComponentRole
ConversationThe thread, with stick to bottom scrolling
Message, Message partsTurns, reasoning, tool calls and the version rows
MarkdownAgent replies
ReasoningThe plan before the build
Write file, Edit file, BashFile writes, edits and the build command in the thread
Prompt inputThe composer, with Stop while building
Context chipsThe element picked in the preview
Web previewThe running app: address, device sizes, reload, console
Preview toolbarSelect and comment over the preview
Artifact versionsStep between versions, with line changes
File treeGenerated files with added and modified markers
Code blockThe open file, streaming while it is written
ResizableThe split between chat and workspace
Empty state, SuggestionsThe 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

PropTypeNotes
chatuseChatcnChat() resultMessages, status and actions in one prop
messages, status, actionsOverride chat, or use any runtime
titleReactNodeProject name in the header
onPublish, publishLabel() => void, ReactNodeShows Publish once there is a version
headerActionsReactNodeExtra controls before Publish
versionsAppBuilderVersion[]Defaults to the data-app-version parts in messages
version, defaultVersion, onVersionChangestringControlled or uncontrolled
onRestoreVersion(id) => voidShows 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
previewUrlstringAddress shown for srcDoc versions. Defaults to localhost:5173
previewErrorstring | { title, message }Shows the error view in the preview
buildingbooleanOverride the building state
visualEditbooleanSelect and comment tools. Default true
formatPrompt(text, element) => stringWords the prompt sent for a picked element
emptyTitle, emptyDescription, suggestionsThe first screen
placeholderstringComposer placeholder
toolRenderersToolRendererMapDefaults to file, search and command renderers
fullHeightbooleanFill the viewport instead of the parent

Keyboard

KeyAction
EnterSend. Shift+Enter adds a line, and nothing sends while an IME is composing
EscapeStop the build while the composer has focus. Clears the preview tool
S, CSelect or comment in the preview, outside text fields
ArrowLeft, ArrowRightMove between Preview and Code, or the narrow tabs
Arrow keys on the dividerResize 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-pressed for 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.