import { z } from 'zod'; import { NextResponse } from 'next/server'; import { researchTurn, listSessions } from '@/lib/ai/research'; import { clientIpHash, consumeQuota, currentUserId, getAnonId } from '@/lib/ai/request'; export const runtime = 'nodejs'; export const maxDuration = 300; const Body = z.object({ sessionId: z.string().regex(/^rs_[a-z0-9]+$/).nullable().optional(), message: z.string().min(1).max(4000) }); /** POST → Server-Sent Events stream of ResearchEvent JSON lines. */ export async function POST(req: Request) { const parsed = Body.safeParse(await req.json().catch(() => null)); if (!parsed.success) return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); const [userId, ipHash, anonId] = await Promise.all([currentUserId(), clientIpHash(), getAnonId()]); const quota = await consumeQuota('research', { userId, ipHash }); if (!quota.ok) return NextResponse.json({ error: `Daily research limit reached (${quota.limit}). Sign in for a higher limit.` }, { status: 429 }); const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const send = (e: unknown) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(e)}\n\n`)); try { for await (const ev of researchTurn({ sessionId: parsed.data.sessionId ?? null, anonId, userId, message: parsed.data.message, signal: req.signal })) send(ev); } catch (err) { send({ type: 'error', message: err instanceof Error ? err.message : String(err) }); } finally { controller.enqueue(encoder.encode('data: [DONE]\n\n')); controller.close(); } }, }); return new Response(stream, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-accel-buffering': 'no' } }); } export async function GET() { const [userId, anonId] = await Promise.all([currentUserId(), getAnonId(false)]); const sessions = await listSessions({ anonId, userId }); return NextResponse.json({ sessions: sessions.map((s) => ({ id: s.id, title: s.title, messageCount: s.messageCount, updatedAt: s.updatedAt })) }); }