ZTS Docs
AI

Chat

Chat UI, streaming API, persistence, and mobile integration notes.

The web chat feature lives at /chat when NEXT_PUBLIC_ENABLE_CHAT_PAGE=true. It uses useChat from @ai-sdk/react with a custom transport that posts to POST /api/chat.

Web UI

FileRole
apps/web/src/app/(app)/(full-page)/chat/page.tsxPage shell
apps/web/src/components/ai/chat-workspace.tsxLists/creates chats via tRPC; loads messages
apps/web/src/components/ai/ai-chat.tsxuseChat, provider/model/intelligence picker
apps/web/src/components/ai/ai-chat-header.tsxHeader + provider Select
apps/web/src/hooks/use-configured-ai-providers.tsProviders available to the current user

Chat workspace flow

  1. chat.list — sidebar of existing conversations.
  2. chat.create — new chat row (title defaults from first message).
  3. chat.get — messages + stored settings (providerId, modelId, intelligence).
  4. User sends a message → useChat streams from /api/chat.
  5. On finish, assistant message is persisted; chat.updateSettings saves provider/model changes.

Streaming API

Route: apps/web/src/app/api/chat/route.ts
Max duration: 60 seconds (export const maxDuration = 60)

Request body

{
  chatId: string;           // required — must belong to the signed-in user
  messages: UIMessage[];    // conversation from useChat
  providerId?: string;      // overrides chat-stored default
  modelId?: string;
  intelligence?: "standard" | "low" | "medium" | "high";
}

Auth and gates

  • Session via Better Auth (auth.api.getSession).
  • Email verification policy via canAccessAuthenticatedApp.
  • Chat row ownership verified before streaming.

Server pipeline

  1. Load user preferences → resolveUserAiProvider
  2. createLanguageModel with user's API key
  3. createZtsAgent with merged tool sets (see Agents)
  4. createAgentUIStreamResponse streams UI messages to the client
  5. onFinish persists the assistant reply and updates chat updatedAt

Provider API keys never leave the server; the client only sends providerId / modelId.

Client transport

AiChat configures DefaultChatTransport to merge chatId, providerId, modelId, and intelligence into every request body:

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
 
const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({
    api: "/api/chat",
    body: {
      chatId,
      providerId: selectedProviderId,
      modelId: selectedModelId,
      intelligence: selectedIntelligence,
    },
  }),
});

Settings changes call api.chat.updateSettings so the next session restores the user's picks.

Mobile

apps/mobile/src/features/chat/ChatPanel.tsx demonstrates useChat against ${getBaseUrl()}/api/chat with session cookies.

Important: The web API requires chatId and validates chat ownership. The mobile demo currently sends only providerId in the transport body. To use the same route on mobile:

  • Create or select a chat via tRPC (chat.create / chat.list) and pass chatId in the transport body, or
  • Add a dedicated mobile-friendly route that auto-creates chats.

Align mobile with the chatRequestSchema in route.ts before shipping chat to production on mobile.

Database

Chat and message rows are managed through tRPC routers under packages/trpc/src/routers/chat.ts (or equivalent). Run migrations after schema changes:

pnpm db:migrate

Troubleshooting

SymptomCheck
No providers in dropdownUser enabled a provider + saved a key in Settings → AI
401 on /api/chatSession cookie / auth state
400 missing chatIdTransport body must include chatId (web workspace does this)
Provider errorsKey validity, provider in ENABLED_AI_PROVIDERS, model in catalog

On this page