Skip to content

Chat

Build a chat where replies mix text and generated UI - with your own state, with the AI SDK's useChat, or with @gistui/chat's store and layouts.

Updated View as Markdown

In a chat, a reply is usually some text and a UI. Use the inline prompt: the model then writes chat text and puts the program in gistui code fences.

const system = prompt({ mode: "inline" }).text;

There are three ways to build the client, from most to least control.

1. Your own chat state

splitReply separates a reply into text parts and UI parts. replyProgram joins all the fences of one reply into one program.

import { replyProgram, splitReply } from "@gistui/chat";

const parts = splitReply(text, { streaming }); // [{ kind: "text", text }, { kind: "ui", source, open }]
const program = replyProgram(parts);

// Render the text parts as chat text, then the UI once:
<GistUI library={ui} source={program} streaming={streaming} onAction={handle} />;

Other code blocks (ts, json) stay chat text.

2. The AI SDK’s useChat

The same two functions work on the text of a useChat message. The full example is in Vercel AI SDK.

3. @gistui/chat

A headless store with model adapters and thread storage, plus React layouts.

npm i @gistui/chat @gistui/react @gistui/styles
import { aiSdk, createChat, localThread } from "@gistui/chat";
import { Chat } from "@gistui/chat/react";
import "@gistui/chat/chat.css";
import "@gistui/styles/styles.css";
import { ui } from "@gistui/react/ui";

const store = createChat({
  adapter: aiSdk({ url: "/api/chat" }),
  storage: localThread("support"),
});

<Chat store={store} library={ui} layout="sidebar" title="Assistant" suggestions={["Show last month's sales"]} />;

Adapters

Adapter Talks to
aiSdk({ url }) A Vercel AI SDK route returning toUIMessageStreamResponse()
openai({ model, url? }) OpenAI Chat Completions and every compatible API
anthropic({ model, url? }) Anthropic Messages
agui({ url }) An AG-UI agent endpoint
textStream({ url }) Any endpoint that streams plain text

Call models from your server. apiKey on an adapter sends the key from the browser, where every visitor can read it: it is for local prototypes only.

The store

createChat({ adapter, system?, initialMessages?, storage?, onError? }) returns a store with send(text), regenerate(), stop(), clear() and a subscribable snapshot of the messages. It has no framework dependency, so it works under Vue, Svelte or Solid with your own markup.

Layouts

<Chat layout> is "full" (a page), "sidebar" or "tray" (a bottom sheet). It takes the same tools, mutations, onToolCall, theme, color, tokens, darkTokens and lockUntil as <GistUI>.

What the chat package handles for you

  • UI actions become messages. A button’s label or a form’s summary goes back as the next message with origin: "ui". The thread labels it “Sent from the UI”, and the model receives it wrapped, so text chosen by a generated program is never mistaken for typed text. Send one yourself with store.send(text, { origin: "ui" }).
  • Only the latest answer refreshes. Earlier answers still load and respond, but their timed queries are paused.
  • History is clean. requestHistory(messages) is what the model receives: failed, empty and still-streaming replies are left out.
  • Threads persist. The thread is saved after each sent message, about once a second while a reply streams, and at the end. A saved error keeps its HTTP status but not the provider’s text.

Editing an earlier UI

To let the model change a UI it already showed instead of writing it again, see Edits and multi-turn.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close