By the end of this page a model’s answer renders as a live interface in your app. You need a model you can call with streaming, and a web app in React, Vue, Svelte, Solid or vanilla JavaScript.
Read https://gistui.com/docs/llms.txt, then install GistUI in this app: add the renderer for my framework, a server route that sends GistUI's system prompt to my model and streams the text back, and a page that renders it.
Install
Install the renderer for your framework, the stylesheet and the catalog. All @gistui/* packages share one version: install and upgrade them together.
npm i @gistui/react @gistui/styles @gistui/catalogyarn add @gistui/react @gistui/styles @gistui/catalogpnpm add @gistui/react @gistui/styles @gistui/catalogbun add @gistui/react @gistui/styles @gistui/catalognpm i @gistui/vue @gistui/styles @gistui/catalogyarn add @gistui/vue @gistui/styles @gistui/catalogpnpm add @gistui/vue @gistui/styles @gistui/catalogbun add @gistui/vue @gistui/styles @gistui/catalognpm i @gistui/svelte @gistui/styles @gistui/catalogyarn add @gistui/svelte @gistui/styles @gistui/catalogpnpm add @gistui/svelte @gistui/styles @gistui/catalogbun add @gistui/svelte @gistui/styles @gistui/catalognpm i @gistui/solid @gistui/styles @gistui/catalogyarn add @gistui/solid @gistui/styles @gistui/catalogpnpm add @gistui/solid @gistui/styles @gistui/catalogbun add @gistui/solid @gistui/styles @gistui/catalognpm i @gistui/vanilla @gistui/styles @gistui/catalogyarn add @gistui/vanilla @gistui/styles @gistui/catalogpnpm add @gistui/vanilla @gistui/styles @gistui/catalogbun add @gistui/vanilla @gistui/styles @gistui/catalogSend the system prompt from your server
The prompt teaches the model GistUI Lang. Build it where you call the model, so it stays out of the browser bundle, and send the model’s text back unchanged.
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().text,
messages: await convertToModelMessages(messages),
});
return result.toTextStreamResponse();
}import { prompt } from "@gistui/catalog";
import { textDeltas, toReadable } from "@gistui/server";
export async function POST(req: Request) {
const { messages } = await req.json();
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-5",
stream: true,
messages: [{ role: "system", content: prompt().text }, ...messages],
}),
});
if (!res.ok) return new Response(await res.text(), { status: res.status });
return new Response(toReadable(textDeltas(res, "openai")));
}import { prompt } from "@gistui/catalog";
import { textDeltas, toReadable } from "@gistui/server";
export async function POST(req: Request) {
const { messages } = await req.json();
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-5-5",
max_tokens: 4096,
stream: true,
system: prompt().text,
messages,
}),
});
if (!res.ok) return new Response(await res.text(), { status: res.status });
return new Response(toReadable(textDeltas(res, "anthropic")));
}The fetch versions use @gistui/server (npm install @gistui/server) to turn a provider’s event stream into plain text. More providers are in Connect a model.
Render the answer
Import the stylesheet once, then give the renderer the response body.
import "@gistui/styles/styles.css";
import { useState } from "react";
import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";
export function Answer() {
const [stream, setStream] = useState<ReadableStream | null>(null);
async function ask(text: string) {
const res = await fetch("/api/answer", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
setStream(res.body);
}
return (
<>
<button onClick={() => ask("A dashboard of last month's sales")}>Ask</button>
{stream && <GistUI library={ui} stream={stream} onAction={(a) => a.type === "send" && ask(a.message)} />}
</>
);
}<script setup lang="ts">
import "@gistui/styles/styles.css";
import { shallowRef } from "vue";
import { GistUI } from "@gistui/vue";
const stream = shallowRef<ReadableStream | null>(null);
async function ask(text: string) {
const res = await fetch("/api/answer", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
stream.value = res.body;
}
</script>
<template>
<button @click="ask('A dashboard of last month\'s sales')">Ask</button>
<GistUI v-if="stream" :stream="stream" @action="(a) => a.type === 'send' && ask(a.message)" />
</template><script lang="ts">
import "@gistui/styles/styles.css";
import { GistUI } from "@gistui/svelte";
let stream = $state.raw<ReadableStream | null>(null);
async function ask(text: string) {
const res = await fetch("/api/answer", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
stream = res.body;
}
</script>
<button onclick={() => ask("A dashboard of last month's sales")}>Ask</button>
{#if stream}
<GistUI {stream} onaction={(a) => a.type === "send" && ask(a.message)} />
{/if}import "@gistui/styles/styles.css";
import { createSignal, Show } from "solid-js";
import { GistUI } from "@gistui/solid";
export function Answer() {
const [stream, setStream] = createSignal<ReadableStream | null>(null);
async function ask(text: string) {
const res = await fetch("/api/answer", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
setStream(res.body);
}
return (
<>
<button onClick={() => ask("A dashboard of last month's sales")}>Ask</button>
<Show when={stream()}>{(s) => <GistUI stream={s()} onAction={(a) => a.type === "send" && ask(a.message)} />}</Show>
</>
);
}import "@gistui/styles/styles.css";
import { mount } from "@gistui/vanilla";
import { ui } from "@gistui/vanilla/ui";
async function ask(text: string) {
const res = await fetch("/api/answer", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
mount(document.getElementById("answer")!, {
library: ui,
stream: res.body!,
onAction: (a) => a.type === "send" && ask(a.message),
});
}
ask("A dashboard of last month's sales");Check that it works
Run the app and ask for “a dashboard of last month’s sales”. The screen fills in while the reply streams, and pressing a follow-up suggestion sends the next question.
If nothing renders
| What you see | Cause | Fix |
|---|---|---|
| Unstyled HTML | The stylesheet is not imported | import "@gistui/styles/styles.css" once, at the top of your app |
| Raw JSON events on screen, or nothing | The route sends provider events, not text | Use textDeltas(res, format) or the AI SDK’s toTextStreamResponse() |
| The model answers in prose | The system prompt is not reaching the model | Pass prompt().text as system (or as the first system message) |
| Components look squashed by your CSS reset | Cascade layers are in the wrong order | See Theming: with Tailwind |
Next
- Actions and forms: what your
onActionreceives. - Chat: replies that mix text and UI, with history.
- Tools and live data: let an answer query your API.