streamText works as it is. Pass GistUI’s prompt as system and choose how to return the stream.
| You render with | Return | Client |
|---|---|---|
<GistUI stream> |
result.toTextStreamResponse() |
stream={res.body} |
useChat from @ai-sdk/react |
result.toUIMessageStreamResponse() |
source + streaming from the message text |
@gistui/chat |
result.toUIMessageStreamResponse() |
the aiSdk() adapter |
npm i ai @gistui/catalogyarn add ai @gistui/catalogpnpm add ai @gistui/catalogbun add ai @gistui/catalogA single answer
import { prompt } from "@gistui/catalog";
import { convertToModelMessages, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: "openai/gpt-5", // a gateway model id, or a provider model such as openai("gpt-5")
system: prompt().text,
messages: await convertToModelMessages(messages),
});
return result.toTextStreamResponse();
}"use client";
import "@gistui/styles/styles.css";
import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";
export function Answer({ stream }: { stream: ReadableStream }) {
return <GistUI library={ui} stream={stream} />;
}A chat with useChat
Use the inline prompt, so a reply is chat text with the UI in gistui code fences. splitReply separates the two.
import { prompt } from "@gistui/catalog";
import { convertToModelMessages, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: "openai/gpt-5",
system: prompt({ mode: "inline" }).text,
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}"use client";
import "@gistui/styles/styles.css";
import { useChat } from "@ai-sdk/react";
import { replyProgram, splitReply } from "@gistui/chat";
import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";
export function Chat() {
const { messages, sendMessage, status } = useChat();
return messages.map((m, i) => {
const text = m.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
if (m.role === "user") return <p key={m.id}>{text}</p>;
const streaming = status === "streaming" && i === messages.length - 1;
const parts = splitReply(text, { streaming }); // [{ kind: "text", text }, { kind: "ui", source, open }]
return (
<div key={m.id}>
{parts.filter((p) => p.kind === "text").map((p, j) => <p key={j}>{p.text}</p>)}
<GistUI
library={ui}
source={replyProgram(parts)}
streaming={streaming}
onAction={(a) => (a.type === "send" || a.type === "submit") && sendMessage({ text: a.message })}
/>
</div>
);
});
}All the fences of one reply form one program, shown where the first fence is.
A chat with @gistui/chat
If you would rather not hold the conversation yourself, @gistui/chat has a store and ready React layouts. The same route as above works.
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" />;See Chat.
Tools
AI SDK tools (tools: { … } in streamText) run on your server while the model is answering, and they keep working unchanged. GistUI’s own tools are different: they are called from the rendered UI, in the browser, by @query. Use them when an answer should keep its data live after the model has finished. See Tools and live data.
Repair on the server
To store the repaired program rather than the raw one, pass the text stream through repairStream:
import { library, prompt } from "@gistui/catalog";
import { repairStream, toReadable } from "@gistui/server";
const result = streamText({ model, system: prompt().text, messages });
return new Response(toReadable(repairStream(result.textStream, library)));