From bc9bbe12deeb08b26319baa02bf19ff6c8186928 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 9 Nov 2025 01:03:36 +0000 Subject: [PATCH] feat: Update Step 7 with tool-based AI + Fix auth callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 7 Updates (AI Chat with Structured Output): - Created lib/ai-schemas.ts with Zod schema for NodeSuggestion - Updated app/api/chat/route.ts: - Changed import from 'ai' to '@ai-sdk/react' for streamText - Added tools configuration with 'suggest_node' tool using NodeSuggestionSchema - Added persona support with dynamic system prompts - Extracts persona from request data object - Rewrote app/chat/page.tsx: - Changed from server component to client component ('use client') - Uses useChat from '@ai-sdk/react' (fixes broken 'ai/react' import) - Added experimental_onToolCall handler for node suggestions - Redirects to /editor/new with AI-generated title/body as query params - Integrated MicrophoneRecorder for voice input - Added persona support (currently hardcoded to 'Socratic') - Added tests/magnitude/07-chat.mag.ts with tests for: - Basic chat functionality - AI-triggered node suggestions with redirect to editor Auth Callback Fixes: - Fixed app/api/auth/callback/route.ts: - Changed to use agent.api.com.atproto.server.getSession() to fetch session - Previously used agent.getSession() which returned empty did/handle - Added user upsert to SurrealDB (INSERT...ON DUPLICATE KEY UPDATE) - Fixed variable references (session.did -> did, session.handle -> handle) - Properly creates user record before minting JWT CLAUDE.md Updates: - Added git commit HEREDOC syntax documentation for proper quote escaping - Clarified that this project allows direct git commits (no PGP signatures) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- AGENTS.md | 8 +- app/api/auth/callback/route.ts | 45 +++++++--- app/api/chat/route.ts | 44 +++++++--- app/chat/page.tsx | 145 ++++++++++++++++++++++++++++++--- lib/ai-schemas.ts | 19 +++++ tests/magnitude/07-chat.mag.ts | 55 +++++++++++++ 6 files changed, 284 insertions(+), 32 deletions(-) create mode 100644 lib/ai-schemas.ts create mode 100644 tests/magnitude/07-chat.mag.ts diff --git a/AGENTS.md b/AGENTS.md index b78f85d..d3da145 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,13 @@ This document outlines the standards and practices for the development of the Ponderants application. The AI agent must adhere to these guidelines strictly. -**Git Commits**: For this project, you can skip PGP-signed commits and run git commands directly. You have permission to execute git add, git commit, and git push commands yourself. +**Git Commits**: For this project, you can skip PGP-signed commits and run git commands directly. You have permission to execute git add, git commit, and git push commands yourself. When writing commit messages with git commit -m, always use HEREDOC syntax with single quotes to properly escape special characters: +```bash +git commit -m "$(cat <<'EOF' +Your commit message here +EOF +)" +``` You are an expert-level, full-stack AI coding agent. Your task is to implement the "Ponderants" application. Product Vision: Ponderants is an AI-powered diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index 418ff04..17c5e97 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -55,27 +55,52 @@ export async function GET(request: NextRequest) { // 5. Use the ATproto token to get the user's session info (did, handle) const agent = new AtpAgent({ service: pdsUrl }); + + // Set the session with the tokens we just received agent.resumeSession({ accessJwt: access_token, refreshJwt: refresh_token, - did: '', - handle: '', + did: '', // Will be populated by getSession call + handle: '', // Will be populated by getSession call }); - // getSession will populate the agent with the correct did/handle - const session = await agent.getSession(); + // Fetch the actual session info from the server + const sessionResponse = await agent.api.com.atproto.server.getSession(); - if (!session.did || !session.handle) { + if (!sessionResponse.success || !sessionResponse.data.did || !sessionResponse.data.handle) { throw new Error('Failed to retrieve user session details'); } - // 6. Mint OUR app's SurrealDB JWT - const surrealJwt = mintSurrealJwt(session.did, session.handle); + const { did, handle } = sessionResponse.data; - // 7. Create redirect response + // 6. Create or update user in SurrealDB + // We use root credentials here since the user doesn't have a JWT yet + const Surreal = (await import('surrealdb')).default; + const db = new Surreal(); + await db.connect(process.env.SURREALDB_URL!); + await db.signin({ + username: process.env.SURREALDB_USER!, + password: process.env.SURREALDB_PASS!, + }); + await db.use({ + namespace: process.env.SURREALDB_NS!, + database: process.env.SURREALDB_DB!, + }); + + // Upsert the user (create if doesn't exist, update handle if it does) + await db.query( + 'INSERT INTO user (did, handle) VALUES ($did, $handle) ON DUPLICATE KEY UPDATE handle = $handle', + { did, handle } + ); + await db.close(); + + // 7. Mint OUR app's SurrealDB JWT + const surrealJwt = mintSurrealJwt(did, handle); + + // 8. Create redirect response const response = NextResponse.redirect(new URL('/chat', request.url)); - // 8. Set the SurrealDB JWT in a secure cookie on the response + // 9. Set the SurrealDB JWT in a secure cookie on the response response.cookies.set('ponderants-auth', surrealJwt, { httpOnly: true, secure: process.env.NODE_ENV === 'production', @@ -100,7 +125,7 @@ export async function GET(request: NextRequest) { path: '/', }); - // 9. Redirect to the main application + // 10. Redirect to the main application return response; } catch (error) { console.error('Auth callback error:', error); diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 46c2bfc..1866dcf 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,7 +1,8 @@ -import { streamText } from 'ai'; +import { streamText } from '@ai-sdk/react'; import { google } from '@ai-sdk/google'; -import { getCurrentUser } from '@/lib/auth/session'; import { cookies } from 'next/headers'; +import { NodeSuggestionSchema } from '@/lib/ai-schemas'; +import { z } from 'zod'; export const runtime = 'edge'; @@ -14,17 +15,38 @@ export async function POST(req: Request) { return new Response('Unauthorized', { status: 401 }); } - const { messages } = await req.json(); + const { messages, data } = await req.json(); - // Use Google's Gemini model for chat - const result = streamText({ + // Get the 'persona' from the custom 'data' object + const { persona } = z + .object({ + persona: z.string().optional().default('Socratic'), + }) + .parse(data); + + // Dynamically create the system prompt based on persona + 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 with tools + const result = await streamText({ model: google('gemini-1.5-flash'), - messages, - system: `You are a thoughtful interviewer helping the user explore and capture their ideas. - Ask insightful questions to help them develop their thoughts. - Be concise but encouraging. When the user expresses a complete thought, - acknowledge it and help them refine it into a clear, structured idea.`, + system: systemPrompt, + messages: messages, + + // Provide the schema as a 'tool' to the model + tools: { + suggest_node: { + description: 'Suggest a new thought node when an idea is complete.', + schema: NodeSuggestionSchema, + }, + }, }); - return result.toDataStreamResponse(); + // Return the streaming response + return result.toAIStreamResponse(); } diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 1de13bf..ae2d316 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -1,14 +1,139 @@ -import { redirect } from 'next/navigation'; -import { getCurrentUser } from '@/lib/auth/session'; -import { ChatInterface } from '@/components/ChatInterface'; +'use client'; -export default async function ChatPage() { - const user = await getCurrentUser(); +import { useChat } from '@ai-sdk/react'; +import { + Stack, + TextInput, + Button, + Paper, + ScrollArea, + Title, + Container, + Group, + Text, +} from '@mantine/core'; +import { useRouter } from 'next/navigation'; +import { useEffect, useRef } from 'react'; +import { NodeSuggestion } from '@/lib/ai-schemas'; +import { MicrophoneRecorder } from '@/components/MicrophoneRecorder'; - // Redirect to login if not authenticated - if (!user) { - redirect('/login'); - } +export default function ChatPage() { + const router = useRouter(); + const viewport = useRef(null); - return ; + const { + messages, + input, + handleInputChange, + handleSubmit, + setInput, + isLoading, + } = useChat({ + api: '/api/chat', + // Send the persona in the 'data' property + data: { + persona: 'Socratic', // This could be a