import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; // --------------------------------------------------------------------------- // DB helpers — atlas_conversations table // --------------------------------------------------------------------------- let tableReady = false; async function ensureTable() { if (tableReady) return; await query(` CREATE TABLE IF NOT EXISTS atlas_conversations ( project_id TEXT PRIMARY KEY, messages JSONB NOT NULL DEFAULT '[]'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); tableReady = true; } async function loadAtlasHistory(projectId: string): Promise { try { await ensureTable(); const rows = await query<{ messages: any[] }>( `SELECT messages FROM atlas_conversations WHERE project_id = $1`, [projectId] ); return rows[0]?.messages ?? []; } catch { return []; } } async function saveAtlasHistory(projectId: string, messages: any[]): Promise { try { await ensureTable(); await query( `INSERT INTO atlas_conversations (project_id, messages, updated_at) VALUES ($1, $2::jsonb, NOW()) ON CONFLICT (project_id) DO UPDATE SET messages = $2::jsonb, updated_at = NOW()`, [projectId, JSON.stringify(messages)] ); } catch (e) { console.error("[atlas-chat] Failed to save history:", e); } } async function savePrd(projectId: string, prdContent: string): Promise { try { await query( `UPDATE fs_projects SET data = data || jsonb_build_object('prd', $2::text, 'stage', 'architecture'), updated_at = NOW() WHERE id = $1`, [projectId, prdContent] ); console.log(`[atlas-chat] PRD saved for project ${projectId}`); } catch (e) { console.error("[atlas-chat] Failed to save PRD:", e); } } // --------------------------------------------------------------------------- // GET — load stored conversation messages for display // --------------------------------------------------------------------------- export async function GET( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const history = await loadAtlasHistory(projectId); // Filter to only user/assistant messages (no system prompts) for display const messages = history .filter((m: any) => m.role === "user" || m.role === "assistant") .map((m: any) => ({ role: m.role as "user" | "assistant", content: m.content as string })); return NextResponse.json({ messages }); } // --------------------------------------------------------------------------- // POST — send message to Atlas // --------------------------------------------------------------------------- export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const { message } = await req.json(); if (!message?.trim()) { return NextResponse.json({ error: "message is required" }, { status: 400 }); } const sessionId = `atlas_${projectId}`; // Load conversation history from DB to persist across agent runner restarts. // Strip tool_call / tool_response messages — replaying them across sessions // causes Gemini to reject the request with a turn-ordering error. const rawHistory = await loadAtlasHistory(projectId); const history = rawHistory.filter((m: any) => (m.role === "user" || m.role === "assistant") && m.content ); // __init__ is a special internal trigger used only when there is no existing history. // If history already exists, ignore the init request (conversation already started). const isInit = message.trim() === "__atlas_init__"; if (isInit && history.length > 0) { return NextResponse.json({ reply: null, alreadyStarted: true }); } try { const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ // For init, send the greeting prompt but don't store it as a user message message: isInit ? "Begin the conversation. Introduce yourself as Atlas and ask what the user is building. Do not acknowledge this as an internal trigger." : message, session_id: sessionId, history, is_init: isInit, }), signal: AbortSignal.timeout(120_000), }); if (!res.ok) { const text = await res.text(); console.error("[atlas-chat] Agent runner error:", text); return NextResponse.json( { error: "Atlas is unavailable. Please try again." }, { status: 502 } ); } const data = await res.json(); // Persist updated history await saveAtlasHistory(projectId, data.history ?? []); // If Atlas finalized the PRD, save it to the project if (data.prdContent) { await savePrd(projectId, data.prdContent); } return NextResponse.json({ reply: data.reply, sessionId, prdContent: data.prdContent ?? null, model: data.model ?? null, }); } catch (err) { console.error("[atlas-chat] Error:", err); return NextResponse.json( { error: "Request timed out or failed. Please try again." }, { status: 500 } ); } } // --------------------------------------------------------------------------- // DELETE — clear Atlas conversation for this project // --------------------------------------------------------------------------- export async function DELETE( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const sessionId = `atlas_${projectId}`; try { await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${sessionId}`, { method: "DELETE" }); } catch { /* runner may be down */ } try { await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]); } catch { /* table may not exist yet */ } return NextResponse.json({ cleared: true }); }