# **File: COMMIT\_07\_CHAT.md** ## **Commit 7: AI Interviewer: UI & Backend** ### **Objective** Build the conversational chat interface using the Vercel AI SDK. This includes: 1. A chat UI (client) that passes a persona in the request body. 2. An API route (server) that receives the persona and injects it into the system prompt. 3. **Crucially:** Using the AI SDK's tool or schema feature to force the AI to return a specific JSON object when it detects a "complete thought".18 This is far more reliable than prompt-based JSON instructions. ### **Implementation Specification** **1\. Define Structured Output Schema (lib/ai-schemas.ts)** Create a file at /lib/ai-schemas.ts to define the Zod schema for the AI's structured output: TypeScript import { z } from 'zod'; /\*\* \* This Zod schema defines the \*only\* structured output \* we want the AI to be able to generate. We will pass \* this to the Vercel AI SDK to guarantee the AI's output \* conforms to this shape. \[18\] \*/ export const NodeSuggestionSchema \= z.object({ action: z.literal('suggest\_node'), title: z .string() .describe('A concise, descriptive title for the thought node.'), body: z .string() .describe('The full, well-structured content of the thought node.'), }); export type NodeSuggestion \= z.infer\; **2\. Create Chat API Route (app/api/chat/route.ts)** Create the file at /app/api/chat/route.ts: TypeScript import { streamText, StreamTextResult, createStreamableValue, } from '@ai-sdk/react'; import { google } from '@ai-sdk/google'; import { createOpenAI } from '@ai-sdk/openai'; import { NextRequest } from 'next/server'; import { NodeSuggestionSchema } from '@/lib/ai-schemas'; import { z } from 'zod'; // Note: Ensure GOOGLE\_API\_KEY is set in your.env.local const googleAi \= google('gemini-1.5-flash'); export async function POST(req: NextRequest) { const { messages, data } \= await req.json(); // Get the 'persona' from the custom 'data' (or 'body') object const { persona } \= z .object({ persona: z.string().optional().default('Socratic'), }) .parse(data); // Dynamically create the system prompt const systemPrompt \= \`You are a ${persona} thought partner. Your goal is to interview the user to help them explore and structure their ideas. When you identify a complete, self-contained idea, you MUST use the 'suggest\_node' tool to propose it as a new "thought node". Do not suggest a node until the idea is fully formed. For all other conversation, just respond as a helpful AI.\`; // Use the Vercel AI SDK's streamText function const result: StreamTextResult \= await streamText({ model: googleAi, system: systemPrompt, messages: messages, // This is the critical part: // We provide the schema as a 'tool' to the model. \[20\] tools: { suggest\_node: { description: 'Suggest a new thought node when an idea is complete.', schema: NodeSuggestionSchema, }, }, }); // Return the streaming response return result.toAIStreamResponse(); } **3\. Create Chat UI (app/chat/page.tsx)** Create the file at /app/chat/page.tsx: TypeScript 'use client'; import { useChat } from '@ai-sdk/react'; import { Stack, TextInput, Button, Paper, ScrollArea, Title, Container, Group, Text, LoadingOverlay, } from '@mantine/core'; import { useRouter } from 'next/navigation'; import { useEffect, useRef } from 'react'; import { NodeSuggestion } from '@/lib/ai-schemas'; export default function ChatPage() { const router \= useRouter(); const viewport \= useRef\(null); const { messages, input, handleInputChange, handleSubmit, data, isLoading, } \= useChat({ // Send the persona in the 'data' (formerly 'body') property \[21\] data: { persona: 'Socratic', // This could be a \ value }, // The 'experimental\_onToolCall' handler is fired when // the AI returns the structured JSON 'suggest\_node' tool. experimental\_onToolCall: (toolCall, appendToolResult) \=\> { if (toolCall.toolName \=== 'suggest\_node') { const { title, body } \= toolCall.args as NodeSuggestion; // Redirect to the editor with the AI-generated draft const query \= new URLSearchParams({ title, body }).toString(); router.push(\`/editor/new?${query}\`); // Return a message to display in the chat return appendToolResult({ toolName: 'suggest\_node', args: { title, body }, result: \`Drafting node: "${title}"\`, }); } }, }); // Auto-scroll to bottom useEffect(() \=\> { viewport.current?.scrollTo({ top: viewport.current.scrollHeight, behavior: 'smooth', }); }, \[messages\]); return ( \ \ Ponderants Interview \</Title\> \<ScrollArea h="100%" style={{ flex: 1 }} viewportRef={viewport} \> \<Stack gap="md" pb="xl"\> {messages.map((m) \=\> ( \<Paper key={m.id} withBorder shadow="md" p="sm" radius="lg" style={{ alignSelf: m.role \=== 'user'? 'flex-end' : 'flex-start', backgroundColor: m.role \=== 'user'? '\#343a40' : '\#212529', }} w="80%" \> \<Text fw={700} size="sm"\>{m.role \=== 'user'? 'You' : 'AI'}\</Text\> \<Text style={{ whiteSpace: 'pre-wrap' }}\>{m.content}\</Text\> \</Paper\> ))} \</Stack\> \</ScrollArea\> \<form onSubmit={handleSubmit}\> \<Paper withBorder p="sm" radius="xl" my="md"\> \<Group\> \<TextInput value={input} onChange={handleInputChange} placeholder="Speak or type your thoughts..." style={{ flex: 1 }} variant="unstyled" disabled={isLoading} /\> \<Button type="submit" radius="xl" loading={isLoading}\> Send \</Button\> {/\* Voice button will go here \*/} \</Group\> \</Paper\> \</form\> \</Container\> ); } ### **Test Specification** **1\. Create Test File (tests/magnitude/07-chat.mag.ts)** Create a file at /tests/magnitude/07-chat.mag.ts: TypeScript import { test } from 'magnitude-test'; test('\[Happy Path\] User can chat with AI', async (agent) \=\> { // Act: Go to chat page await agent.act('Navigate to /chat'); // Check: Ensure the initial state is correct await agent.check('The title "Ponderants Interview" is visible'); await agent.check('The chat input field is empty'); // Act: Send a message await agent.act( 'Enter "I have an idea about decentralized social media" into the chat input' ); await agent.act('Click the "Send" button'); // Check: User's message appears await agent.check( 'The message "I have an idea about decentralized social media" appears in the chat list' ); // Check: AI response appears (mocked) // We mock the /api/chat response to return a simple text stream await agent.check( 'A new message from "AI" appears in the chat list with a response' ); }); test(' AI can trigger a node suggestion', async (agent) \=\> { // Act: Go to chat page await agent.act('Navigate to /chat'); // Act: Send a message that should trigger a node await agent.act( 'Enter "I think I have a fully formed thought: ATproto is the future of the internet because it separates data from the application." into the chat input' ); // We mock the /api/chat response to return the 'suggest\_node' tool call // with specific 'title' and 'body' arguments. await agent.act('Click the "Send" button'); // Check: The 'experimental\_onToolCall' handler should fire // and redirect the user to the editor. await agent.check( 'The browser URL is now "http://localhost:3000/editor/new"' ); // Check: The editor page is pre-filled with the AI-generated content await agent.check( 'The page URL contains the query parameter "title=ATproto: The Future of the Internet"' ); await agent.check( 'The page URL contains the query parameter "body=ATproto is the future..."' ); });