From a1e6c863d5002e0f43916ce0d16500ff63ccd283 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 2 Jun 2026 00:23:21 -0500 Subject: [PATCH] feat(web): wire AI chat page to streaming /api/chat handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../web/app/(app)/[workspaceSlug]/ai/page.tsx | 176 +++++++++++------- apps/web/app/api/chat/route.ts | 101 ++++++++++ apps/web/package.json | 1 + .../Task-wire-ai-chat-to-trpc.md | 30 +-- pnpm-lock.yaml | 3 + 5 files changed, 236 insertions(+), 75 deletions(-) create mode 100644 apps/web/app/api/chat/route.ts diff --git a/apps/web/app/(app)/[workspaceSlug]/ai/page.tsx b/apps/web/app/(app)/[workspaceSlug]/ai/page.tsx index bdc0759..766fec9 100644 --- a/apps/web/app/(app)/[workspaceSlug]/ai/page.tsx +++ b/apps/web/app/(app)/[workspaceSlug]/ai/page.tsx @@ -1,72 +1,56 @@ "use client"; -import { useState, useRef, useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useParams } from "next/navigation"; -import { Loader2, Send, Sparkles, Bot, User } from "lucide-react"; +import { useChat } from "@ai-sdk/react"; +import { AlertTriangle, Bot, Loader2, Send, Sparkles, User } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -type Message = { - id: string; - role: "user" | "assistant"; - content: string; -}; - export default function AIPage() { const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = + typeof params?.workspaceSlug === "string" ? params.workspaceSlug : ""; - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); - // Auto-scroll on new messages + const { + messages, + input, + handleInputChange, + handleSubmit, + status, + error, + setInput, + } = useChat({ + api: "/api/chat", + body: { workspace: workspaceSlug }, + }); + + const isBusy = status === "submitted" || status === "streaming"; + useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [messages]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const trimmed = input.trim(); - if (!trimmed || isLoading) return; - - const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: trimmed }; - setMessages((prev) => [...prev, userMsg]); - setInput(""); - setIsLoading(true); - - // Placeholder AI response - setTimeout(() => { - const assistantMsg: Message = { - id: crypto.randomUUID(), - role: "assistant", - content: - "I'm your AI assistant. Full AI integration is coming soon! I'll be able to help you create tasks, documents, and manage your workspace.", - }; - setMessages((prev) => [...prev, assistantMsg]); - setIsLoading(false); - }, 1000); - }; + }, [messages, status]); return (
- {/* Header */}

AI Assistant

-

Ask me anything about your workspace

+

+ Ask me anything about your workspace +

- {/* Messages */}
{messages.length === 0 ? (
@@ -76,22 +60,25 @@ export default function AIPage() {

How can I help you today?

- Ask me to create tasks, summarize documents, or manage your workspace. + Ask me to break down a task, draft an outline, or summarize what + you have open.

- {["Create a new project", "Summarize my tasks", "Help me plan a sprint"].map( - (suggestion) => ( - - ), - )} + {[ + "Break this project into subtasks", + "Summarize my open tasks", + "Draft a sprint plan for this week", + ].map((suggestion) => ( + + ))}
) : ( @@ -99,19 +86,28 @@ export default function AIPage() { {messages.map((msg) => (
- {msg.role === "user" ? : } + {msg.role === "user" ? ( + + ) : ( + + )}
))} - {isLoading && ( + {status === "submitted" && (
@@ -133,26 +129,40 @@ export default function AIPage() { )}
)} + + {error ? : null}
- {/* Input */}
-
+