// Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: chat.spboucher.ai import { NextResponse, type NextRequest } from "next/server"; import { requireSession } from "@/lib/auth/guard"; import { ensureCatalogFresh, getModel } from "@/lib/catalog"; import { createConversation, getConversation, getMessage, insertMessage, updateConversation, } from "@/lib/conversations"; import { startGeneration } from "@/lib/generate"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; interface ChatRequestBody { conversationId?: string; content?: string; modelId?: string; /** Assistant message ID to regenerate: creates a sibling branch, never destroys the original. */ regenerateOf?: string; /** Parent message for the new user message (branch editing); defaults to the current leaf. */ parentId?: string | null; } export async function POST(req: NextRequest) { const { unauthorized } = await requireSession(); if (unauthorized) return unauthorized; let body: ChatRequestBody; try { body = await req.json(); } catch { return NextResponse.json({ error: "Invalid request." }, { status: 400 }); } if (!body.modelId) { return NextResponse.json({ error: "Pick a model first." }, { status: 400 }); } await ensureCatalogFresh().catch(() => {}); const model = getModel(body.modelId); if (!model) { return NextResponse.json({ error: "Unknown model. Refresh the catalog." }, { status: 400 }); } let parentMessage = null; if (body.regenerateOf) { // Regeneration: new assistant sibling under the same user message. const original = getMessage(body.regenerateOf); if (!original || original.role !== "assistant") { return NextResponse.json({ error: "Message to regenerate was not found." }, { status: 404 }); } if (!getConversation(original.conversation_id)) { return NextResponse.json({ error: "Conversation not found." }, { status: 404 }); } parentMessage = original.parent_id ? getMessage(original.parent_id) : null; const started = startGeneration({ conversationId: original.conversation_id, parentMessage, model, }); return sseResponse(started.stream); } const content = (body.content ?? "").trim(); if (!content) { return NextResponse.json({ error: "Write a message first." }, { status: 400 }); } // New message flow: resolve or create the conversation. let conversation = body.conversationId ? getConversation(body.conversationId) : null; if (body.conversationId && !conversation) { return NextResponse.json({ error: "Conversation not found." }, { status: 404 }); } if (!conversation) { conversation = createConversation(content); } else if (conversation.title === "New conversation") { updateConversation(conversation.id, { title: content.slice(0, 80) }); } const parentId = body.parentId !== undefined ? body.parentId : conversation.current_leaf_id; if (parentId) { const parent = getMessage(parentId); if (!parent || parent.conversation_id !== conversation.id) { return NextResponse.json({ error: "Invalid parent message." }, { status: 400 }); } } const userMessage = insertMessage({ conversationId: conversation.id, parentId: parentId ?? null, role: "user", content, status: "completed", }); const started = startGeneration({ conversationId: conversation.id, parentMessage: userMessage, model, }); return sseResponse(started.stream, { "X-Conversation-Id": conversation.id, "X-User-Message-Id": userMessage.id, }); } function sseResponse(stream: ReadableStream, extraHeaders?: Record) { 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", ...extraHeaders, }, }); }