feat: add Atlas discovery chat UI and API route

- components/AtlasChat.tsx — conversational PRD discovery UI (violet theme)
- app/api/projects/[projectId]/atlas-chat/route.ts — proxy + DB persistence
- overview/page.tsx — show Atlas for new projects, Orchestrator once PRD done

Made-with: Cursor
This commit is contained in:
2026-03-01 15:56:32 -08:00
parent 35675b7d86
commit 26a11412b5
3 changed files with 495 additions and 5 deletions

View File

@@ -0,0 +1,164 @@
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<any[]> {
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<void> {
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<void> {
try {
await query(
`UPDATE fs_projects
SET data = data || jsonb_build_object('prd', $2, '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);
}
}
// ---------------------------------------------------------------------------
// 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
const history = await loadAtlasHistory(projectId);
try {
const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message,
session_id: sessionId,
history,
}),
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 });
}