- 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
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
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 });
|
|
}
|