Guides
Surfaces
AI chat
An agent chat that runs on a scripted mock, with two interfaces between it and a real model and database.The chat surface is the interface of an AI assistant: a sidebar of chats
grouped into pinned, projects and recents, a conversation where agent steps and
widgets stream in, and a resizable side panel with the assistant's mascot, the
latest run and the widgets the chat has made. A chat's model, instructions and
theme are in its settings dialog. Below the desktop breakpoint the panels become
drawers.
It serves /chat and requires no slices. Chats live in localStorage and
answers come from a mock model in the browser, so it runs on zero environment
variables and no API key. Without the landing surface, / redirects to
/chat.
Everything the UI needs from outside goes through two interfaces in
src/lib/chat, and the hook that drives the UI, useChats, reads only them.
Where chats live is ChatStore, in store.ts:
It ships as localChatStore, seeded with sample chats. Keeping chats in a
database, per user or per organization, is a new object with this shape
assigned to chatStore in the same file.
Where answers come from is ChatModel, in model.ts:
stream yields ChatEvents: text deltas, steps (a step sent again with
the same id updates it), widgets, and a final usage with the tokens the
answer used. The surface ships mockModel, which plays canned agent runs
picked by what the message is about, and quickModel, the same without steps.
The surface ships one real model, Claude, through the route in
src/app/(chat)/api/chat/route.ts. Set ANTHROPIC_API_KEY and it appears in
the model picker; without the key the picker keeps the mocks and nothing
calls out. CHAT_CLAUDE_MODEL picks the model (claude-opus-5-5 by default,
at low effort, which suits chat). The key stays on the server: the browser
posts the conversation and reads back ChatEvents, one JSON object a line.
Every answer is billed to the deployment's key, so the route is not an open
relay. src/lib/chat/access.ts lets a request through only when:
Last updated October 23, 2026- it comes from the site's own pages (its Origin is this host);
- someone is signed in, when Supabase is configured (demo mode has no accounts and skips this);
- the Upstash rate limit allows it: 10 answers a minute and 200 a day per user, or per IP in demo mode. In production the route answers 503 and the picker hides Claude until UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are set; in development it runs without them.
Edit this page on GitHub