Files
vibn-frontend/app/api/projects/[projectId]/atlas-chat/route.ts
Mark Henderson f47205c473 rename: replace all user-facing 'Atlas' references with 'Vibn'
Updated UI text in: project-shell (tab label), AtlasChat (sender name),
FreshIdeaMain, TypeSelector, MigrateSetup, ChatImportSetup, FreshIdeaSetup,
CodeImportSetup, prd/page, build/page, projects/page, deployment/page,
activity/page, layout (page title/description), atlas-chat API route.
Code identifiers (AtlasChat component name, file names) unchanged.

Made-with: Cursor
2026-03-17 16:25:41 -07:00

205 lines
6.6 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::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 Vibn 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: "Vibn 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 });
}