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:
164
app/api/projects/[projectId]/atlas-chat/route.ts
Normal file
164
app/api/projects/[projectId]/atlas-chat/route.ts
Normal 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 });
|
||||
}
|
||||
Reference in New Issue
Block a user