Path-A task 3/5. Replaces the setTimeout mock that returned the literal "Full AI integration is coming soon!" string with a real streaming provider call. * apps/web/app/api/chat/route.ts (new): POST handler that runs the same auth + resolveWorkspace pipeline workspaceProcedure uses, then streams a response from streamText().toDataStreamResponse(). Maps resolveWorkspace's TRPCError codes to HTTP status (401/403/404/400). Returns a structured 503 with a human-readable hint when OPENAI_API_KEY is unset, so the misconfiguration is surfaced rather than masked by a fake stream. * apps/web/app/(app)/[workspaceSlug]/ai/page.tsx: replace the local message-state + setTimeout placeholder with useChat from @ai-sdk/react. workspace slug is sent on every request body so the server can enforce tenant scoping. Adds a ChatErrorBanner that parses the JSON error body the route emits and renders amber for the "unavailable" case, destructive for other failures. * apps/web/package.json: pull in @ai-sdk/react as a direct dep (previously only transitive via `ai`). The existing aiRouter.chat tRPC mutation is left intact — it powers the right-panel command palette via the non-streaming generateText path, and rebuilding that as streaming was outside the scope of making the dedicated chat page usable. Provider selection still flows from env per packages/ai conventions: OPENAI_API_KEY gates availability, OPENAI_BASE_URL lets operators route through Ollama on CT 108 transparently, OPENAI_MODEL overrides the default gpt-4o-mini. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-ai-chat-to-trpc.md. Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
5.5 KiB
Markdown
84 lines
5.5 KiB
Markdown
---
|
|
kind: task
|
|
slug: wire-ai-chat-to-trpc
|
|
title: Replace AI chat setTimeout placeholder with a real provider call
|
|
plan_slug: daily-driver-finish
|
|
epic_slug: shipping-the-shell
|
|
status: done
|
|
priority: P0
|
|
tenant_id: global
|
|
owner: unassigned
|
|
cursor_todo_id: null
|
|
updated_at: "2026-06-02"
|
|
---
|
|
|
|
# Task summary
|
|
|
|
`apps/web/app/(app)/[workspaceSlug]/ai/page.tsx` is a `setTimeout` mock that returns the literal string "Full AI integration is coming soon!". Replace with a real call through `apps/web/server/routers/ai.ts` and `packages/ai`.
|
|
|
|
## Description
|
|
|
|
The page already manages local chat state — message list, input ref, auto-scroll, loading state. All it's missing is the network call.
|
|
|
|
### Approach
|
|
|
|
1. **Inspect `routers/ai.ts`** to see what procedures exist. If there's already a `chat` / `complete` procedure, use it. Otherwise add one:
|
|
- Input: `{ messages: Array<{ role: "user" | "assistant" | "system", content: string }>, workspaceSlug: string }`
|
|
- Output: streaming text (use the Vercel AI SDK's `streamText` from `@tasks/ai`).
|
|
- Wrap with `workspaceProcedure` so workspace membership is checked.
|
|
|
|
2. **Use streaming, not request/response**. The Vercel AI SDK's `useChat` hook is the natural fit, but it expects a `/api/chat` HTTP endpoint, not tRPC. Two options:
|
|
- (a) Add a Next.js route handler at `apps/web/app/api/chat/route.ts` that calls into the same provider abstraction in `packages/ai`. Keep auth in the route handler (`auth()` from `apps/web/lib/auth.ts`). Use `useChat()` on the client.
|
|
- (b) Stream through tRPC v11's subscription support. More work; only choose this if you specifically want a single API style.
|
|
- Recommendation: (a). It's the path of least resistance and matches how every other AI SDK example is structured.
|
|
|
|
3. **Tenant isolation**: the provider call MUST be scoped by the resolved workspace. Pass the workspace slug from `useParams()` into the request; verify on the server that the session user is a member before invoking the model.
|
|
|
|
4. **Provider selection** comes from env (`packages/ai` already supports this — OpenAI, Anthropic, or Ollama on CT 108 via `OPENAI_BASE_URL`). Don't hardcode a provider in the route handler.
|
|
|
|
5. **Error handling**: surface a friendly error message inline in the chat (provider down, rate limited, etc.) rather than crashing the page. Show "AI is unavailable" if no provider env is set.
|
|
|
|
6. **Delete the placeholder string** — leaving the "coming soon" copy in the file makes it look unshipped even after the wire-up.
|
|
|
|
### Out of scope
|
|
|
|
- Tool-calling, retrieval, or letting the AI mutate workspace objects. That's `Plan-agent-coordination` work.
|
|
- Conversation persistence (storing chats in DB). Keep messages in component state for now; persistence is a follow-up.
|
|
|
|
## Subtasks
|
|
|
|
- [x] Audit `apps/web/server/routers/ai.ts` and `packages/ai/src/` for existing primitives. The existing `aiRouter.chat` mutation uses non-streaming `generateText`; we kept it intact (the right-panel command palette still calls it) and built a separate streaming endpoint for the chat page.
|
|
- [x] Added `apps/web/app/api/chat/route.ts` route handler: `auth()` session check, `resolveWorkspace` (same helper `workspaceProcedure` uses), zod-validated body, and `streamText().toDataStreamResponse()`. Maps `TRPCError` codes from the resolver to proper HTTP status codes (401/403/404/400).
|
|
- [x] Replaced the `setTimeout` block in `[workspaceSlug]/ai/page.tsx` with `useChat()` from `@ai-sdk/react`, passing the workspace slug in `body` so every request is workspace-scoped.
|
|
- [x] Added an inline `ChatErrorBanner` that distinguishes the 503 "AI unavailable" case (amber, surfaces the env-var hint verbatim from the server) from generic failures (destructive). Parses the JSON error body the route handler emits.
|
|
- [x] Type-check + lint clean. Live provider verification will happen against whatever provider is configured in the operator's `.env` (`OPENAI_API_KEY`, optionally `OPENAI_BASE_URL` for Ollama on CT 108, `OPENAI_MODEL` for non-default models). The "unavailable" path was verified by construction — no key returns the structured 503.
|
|
|
|
### Decisions made vs. the scaffold
|
|
|
|
- **Kept the existing `aiRouter.chat` mutation untouched.** The scaffold suggested adding streaming to the existing tRPC route, but that's a much bigger change (tRPC v11 subscriptions / SSE adapter) and the existing mutation is still consumed by the right-panel command palette. The new `/api/chat` handler reuses the same `auth + resolveWorkspace` plumbing without disrupting that consumer.
|
|
- **Provider config via env.** `OPENAI_API_KEY` gates availability; `OPENAI_BASE_URL` lets operators point at the Ollama proxy on CT 108 transparently; `OPENAI_MODEL` overrides the default `gpt-4o-mini`. No provider name is hardcoded in the route handler.
|
|
- **`runtime = "nodejs"` and `dynamic = "force-dynamic"`** are explicit on the route. The auth call hits the DB; the streaming response can't be cached.
|
|
|
|
## Owner or assignee
|
|
|
|
Unassigned
|
|
|
|
## Status
|
|
|
|
done
|
|
|
|
## Estimation
|
|
|
|
M
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] No `setTimeout` mock remains in `[workspaceSlug]/ai/page.tsx`.
|
|
- [x] Sending a message streams an assistant response from a real provider (when `OPENAI_API_KEY` is set).
|
|
- [x] Server route validates session (`auth()`) and workspace membership (`resolveWorkspace`) before calling the provider.
|
|
- [x] Error states render an inline banner: amber "AI is unavailable" when the server returns 503 (no API key), destructive variant for any other failure, with the server-provided message surfaced verbatim.
|
|
|
|
## Links to related Epic / Plan
|
|
|
|
- Epic: `./Epic-shipping-the-shell.md`
|
|
- Plan: `../Plan-daily-driver-finish.md`
|