import { NextRequest } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; const MODEL = process.env.GEMINI_MODEL ?? 'gemini-2.0-flash'; function buildSystemPrompt(projectData: Record): string { const name = (projectData.name as string) ?? 'this project'; const description = (projectData.description as string) ?? ''; const vision = (projectData.vision as string) ?? ''; const giteaRepo = (projectData.giteaRepo as string) ?? ''; return `You are the personal AI COO for "${name}" — a product assistant that thinks like a seasoned Chief Operating Officer. Your role is to be the founder's single AI interface. The founder talks to you. You figure out what needs to happen and make it clear before anything is done. ## About this project ${description ? `Description: ${description}` : ''} ${vision ? `Vision: ${vision}` : ''} ${giteaRepo ? `Codebase: ${giteaRepo}` : ''} ## Your principles - You are NOT a chatbot. You are an executive partner. - You reason across the entire business: product, code, growth, analytics, operations. - You surface insights the founder hasn't asked about when they're relevant. - You speak in plain language. No jargon without explanation. - Before any work is delegated, you surface a clear plan: "Here's what I'd do: [plain language]. Want me to start?" - You are brief and direct. Founders are busy. - When something needs to be built, you describe the scope before acting. - When something needs analysis, you explain what you found and what it means. - You never ask the founder to pick a module, agent, or technical approach — you figure that out. ## What you have access to (context expands over time) - The project codebase and commit history - Agent session outcomes (what's been built) - Analytics and deployment status - Market and competitor knowledge (via web search) - Persistent memory of decisions, preferences, patterns ## Tone Confident, brief, thoughtful. Like a trusted advisor who has seen this before. Not overly formal, not casual. Never sycophantic — skip "Great question!" entirely. Start each conversation ready to help. If this is the user's first message, greet them briefly and ask what's on their mind.`.trim(); } export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const { projectId } = await params; const session = await getServerSession(authOptions); if (!session?.user?.email) { return new Response('Unauthorized', { status: 401 }); } const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY ?? ''; const STREAM_URL = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:streamGenerateContent?key=${GOOGLE_API_KEY}&alt=sse`; if (!GOOGLE_API_KEY) { return new Response('GOOGLE_API_KEY not configured', { status: 500 }); } const { message, history = [] } = await req.json() as { message: string; history: Array<{ role: 'user' | 'model'; content: string }>; }; if (!message?.trim()) { return new Response('Message required', { status: 400 }); } // Load project data for context let projectData: Record = {}; try { const rows = await query<{ data: Record }>( `SELECT p.data FROM fs_projects p JOIN fs_users u ON u.id = p.user_id WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`, [projectId, session.user.email] ); if (rows.length > 0) projectData = rows[0].data ?? {}; } catch { /* best-effort, proceed without context */ } const systemPrompt = buildSystemPrompt(projectData); // Build Gemini conversation history const contents = [ ...history.map(h => ({ role: h.role, parts: [{ text: h.content }], })), { role: 'user' as const, parts: [{ text: message }] }, ]; // Call Gemini streaming const geminiRes = await fetch(STREAM_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_instruction: { parts: [{ text: systemPrompt }] }, contents, generationConfig: { temperature: 0.7, maxOutputTokens: 1024, }, }), }); if (!geminiRes.ok) { const err = await geminiRes.text(); return new Response(`Gemini error: ${err}`, { status: 502 }); } // Proxy the SSE stream, extracting just the text deltas const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const reader = geminiRes.body!.getReader(); const decoder = new TextDecoder(); let buf = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const lines = buf.split('\n'); buf = lines.pop() ?? ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const json = line.slice(6).trim(); if (!json || json === '[DONE]') continue; try { const chunk = JSON.parse(json); const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text; if (text) controller.enqueue(encoder.encode(text)); } catch { /* skip malformed chunk */ } } } } finally { controller.close(); } }, }); return new Response(stream, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); }