Skip to content

Quickstart

Install GistUI, give your model the system prompt, and render its streamed answer as UI.

Updated View as Markdown

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/catalog
npm i @gistui/vue @gistui/styles @gistui/catalog
npm i @gistui/svelte @gistui/styles @gistui/catalog
npm i @gistui/solid @gistui/styles @gistui/catalog
npm i @gistui/vanilla @gistui/styles @gistui/catalog

Send 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.

app/api/answer/route.tsts
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();
}
app/api/answer/route.tsts
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")));
}
app/api/answer/route.tsts
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.

Answer.tsxtsx
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)} />}
    </>
  );
}
Answer.vuevue
<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>
Answer.sveltesvelte
<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}
Answer.tsxtsx
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>
    </>
  );
}
answer.tsts
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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close