feat(build): AI architecture recommendation with review + confirm flow
- New /api/projects/[projectId]/architecture (GET/POST/PATCH) — reads PRD + phases, calls AI to generate structured monorepo architecture JSON, persists to fs_projects.data.architecture; PATCH sets confirmed flag - Rebuilt Build tab to show the AI-generated recommendation: expandable app cards (tech stack, key screens), shared packages, infrastructure, integrations, and risk notes; confirm button + "adjustable later" note Made-with: Cursor
This commit is contained in:
212
app/api/projects/[projectId]/architecture/route.ts
Normal file
212
app/api/projects/[projectId]/architecture/route.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET — return saved architecture (if it exists)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
||||
[projectId]
|
||||
);
|
||||
const data = rows[0]?.data ?? {};
|
||||
return NextResponse.json({
|
||||
architecture: data.architecture ?? null,
|
||||
prd: data.prd ?? null,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ architecture: null, prd: null });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST — generate architecture recommendation from PRD using AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 body = await req.json().catch(() => ({}));
|
||||
const forceRegenerate = body.forceRegenerate === true;
|
||||
|
||||
// Load project PRD + phases
|
||||
let prd: string | null = null;
|
||||
let phases: any[] = [];
|
||||
|
||||
try {
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
||||
[projectId]
|
||||
);
|
||||
const data = rows[0]?.data ?? {};
|
||||
prd = data.prd ?? null;
|
||||
|
||||
// Return cached architecture if it exists and not forcing regenerate
|
||||
if (data.architecture && !forceRegenerate) {
|
||||
return NextResponse.json({ architecture: data.architecture, cached: true });
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!prd) {
|
||||
return NextResponse.json({ error: "No PRD found — complete discovery first" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const phaseRows = await query<{ phase: string; title: string; summary: string; data: any }>(
|
||||
`SELECT phase, title, summary, data FROM atlas_phases WHERE project_id = $1 ORDER BY saved_at ASC`,
|
||||
[projectId]
|
||||
);
|
||||
phases = phaseRows;
|
||||
} catch { /* phases optional */ }
|
||||
|
||||
// Build a concise context string from phases
|
||||
const phaseContext = phases.map(p =>
|
||||
`## ${p.title}\n${p.summary}\n${JSON.stringify(p.data, null, 2)}`
|
||||
).join("\n\n");
|
||||
|
||||
const prompt = `You are a senior software architect. Analyse the following Product Requirements Document and recommend a technical architecture for a Turborepo monorepo.
|
||||
|
||||
Return ONLY a valid JSON object (no markdown, no explanation) with this exact structure:
|
||||
{
|
||||
"productName": "string",
|
||||
"productType": "string (e.g. PWA Game, SaaS, Marketplace, Internal Tool)",
|
||||
"summary": "2-3 sentence plain-English summary of the recommended architecture",
|
||||
"apps": [
|
||||
{
|
||||
"name": "string (e.g. web, api, simulator)",
|
||||
"type": "string (e.g. Next.js 15, Express API, Node.js service)",
|
||||
"description": "string — what this app does",
|
||||
"tech": ["string array of key technologies"],
|
||||
"screens": ["string array — key screens/routes if applicable, else empty"]
|
||||
}
|
||||
],
|
||||
"packages": [
|
||||
{
|
||||
"name": "string (e.g. db, types, ui)",
|
||||
"description": "string — what this shared package contains"
|
||||
}
|
||||
],
|
||||
"infrastructure": [
|
||||
{
|
||||
"name": "string (e.g. PostgreSQL, Redis, Background Jobs)",
|
||||
"reason": "string — why this is needed based on the PRD"
|
||||
}
|
||||
],
|
||||
"integrations": [
|
||||
{
|
||||
"name": "string (e.g. Ad Network SDK)",
|
||||
"required": true,
|
||||
"notes": "string"
|
||||
}
|
||||
],
|
||||
"designSurfaces": ["string array — e.g. Web App, Mobile PWA, Admin"],
|
||||
"riskNotes": ["string array — 1-2 key architectural risks from the PRD"]
|
||||
}
|
||||
|
||||
Be specific to this product. Do not use generic boilerplate — base your decisions on the PRD content.
|
||||
|
||||
--- DISCOVERY PHASES ---
|
||||
${phaseContext}
|
||||
|
||||
--- PRD ---
|
||||
${prd}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
message: prompt,
|
||||
session_id: `arch_${projectId}_${Date.now()}`,
|
||||
history: [],
|
||||
is_init: false,
|
||||
tools: [], // no tools needed — just structured generation
|
||||
}),
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Agent runner responded ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const raw = data.reply ?? "";
|
||||
|
||||
// Extract JSON from response (strip any accidental markdown)
|
||||
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) throw new Error("No JSON in response");
|
||||
|
||||
const architecture = JSON.parse(jsonMatch[0]);
|
||||
|
||||
// Persist to project data
|
||||
await query(
|
||||
`UPDATE fs_projects
|
||||
SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architecture}', $2::jsonb, true),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[projectId, JSON.stringify(architecture)]
|
||||
);
|
||||
|
||||
return NextResponse.json({ architecture, cached: false });
|
||||
} catch (err) {
|
||||
console.error("[architecture] Generation failed:", err);
|
||||
return NextResponse.json(
|
||||
{ error: "Architecture generation failed. Please try again." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATCH — confirm architecture (sets architectureConfirmed flag)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function PATCH(
|
||||
_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;
|
||||
|
||||
try {
|
||||
await query(
|
||||
`UPDATE fs_projects
|
||||
SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architectureConfirmed}', 'true'::jsonb, true),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[projectId]
|
||||
);
|
||||
return NextResponse.json({ confirmed: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Failed to confirm" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user