feat(ai): automate end-to-end PRD, architecture, and task generation directly from Objective
This commit is contained in:
@@ -14,7 +14,7 @@ type Plan = {
|
|||||||
decisions: Array<{ id: string; title: string; choice: string; why?: string }>;
|
decisions: Array<{ id: string; title: string; choice: string; why?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Tab = "objective" | "prd";
|
type Tab = "objective" | "prd" | "delegate";
|
||||||
|
|
||||||
// Shared Theme Variables
|
// Shared Theme Variables
|
||||||
const INK = {
|
const INK = {
|
||||||
@@ -79,6 +79,7 @@ export default function PlanPageV2() {
|
|||||||
}}>
|
}}>
|
||||||
<TabButton active={activeTab === "objective"} onClick={() => setActiveTab("objective")} icon={<Target size={14} />} label="Objective" />
|
<TabButton active={activeTab === "objective"} onClick={() => setActiveTab("objective")} icon={<Target size={14} />} label="Objective" />
|
||||||
<TabButton active={activeTab === "prd"} onClick={() => setActiveTab("prd")} icon={<FileText size={14} />} label="PRD" />
|
<TabButton active={activeTab === "prd"} onClick={() => setActiveTab("prd")} icon={<FileText size={14} />} label="PRD" />
|
||||||
|
<TabButton active={activeTab === "delegate"} onClick={() => setActiveTab("delegate")} icon={<Layers size={14} />} label="Delegate" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,6 +96,10 @@ export default function PlanPageV2() {
|
|||||||
<PRDView plan={plan} projectId={projectId} onChange={setPlan} />
|
<PRDView plan={plan} projectId={projectId} onChange={setPlan} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: activeTab === "delegate" ? "block" : "none" }}>
|
||||||
|
<DelegateView plan={plan} projectId={projectId} onChange={setPlan} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,13 +153,38 @@ function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: s
|
|||||||
}}
|
}}
|
||||||
placeholder="Describe the business objective..."
|
placeholder="Describe the business objective..."
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: "12px 20px", background: INK.bgHover, borderTop: `1px solid ${INK.border}` }}>
|
<div style={{ display: "flex", justifyContent: "space-between", padding: "12px 20px", background: INK.bgHover, borderTop: `1px solid ${INK.border}` }}>
|
||||||
<button onClick={() => setEditing(false)} className="btn-secondary">Cancel</button>
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!confirm("This will overwrite the PRD and Execution Plan based on the current objective. Continue?")) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/projects/${projectId}/plan/generate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ objective: draft }),
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.plan) onChange(d.plan);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
className="btn-secondary"
|
||||||
|
style={{ background: "#fff", borderColor: INK.border }}
|
||||||
|
>
|
||||||
|
{saving ? "Generating PRD..." : "Generate Complete PRD"}
|
||||||
|
</button>
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
<button onClick={() => setEditing(false)} className="btn-ghost">Cancel</button>
|
||||||
<button onClick={save} disabled={saving} className="btn-primary">
|
<button onClick={save} disabled={saving} className="btn-primary">
|
||||||
{saving ? "Saving..." : "Save Objective"}
|
{saving ? "Saving..." : "Save Objective"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-prose" style={{ background: "#fff", border: `1px solid ${INK.border}`, padding: 32, borderRadius: 8, minHeight: 200 }}>
|
<div className="markdown-prose" style={{ background: "#fff", border: `1px solid ${INK.border}`, padding: 32, borderRadius: 8, minHeight: 200 }}>
|
||||||
{plan.vision ? (
|
{plan.vision ? (
|
||||||
@@ -224,26 +254,7 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string,
|
|||||||
>
|
>
|
||||||
<GitBranch size={16} /> Tech Architecture
|
<GitBranch size={16} /> Tech Architecture
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveDoc("tasks")}
|
|
||||||
style={{
|
|
||||||
textAlign: "left",
|
|
||||||
padding: "12px 16px",
|
|
||||||
borderRadius: 8,
|
|
||||||
background: activeDoc === "tasks" ? "#fff" : "transparent",
|
|
||||||
border: activeDoc === "tasks" ? `1px solid ${INK.border}` : "1px solid transparent",
|
|
||||||
boxShadow: activeDoc === "tasks" ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
|
|
||||||
color: activeDoc === "tasks" ? INK.main : INK.muted,
|
|
||||||
fontWeight: activeDoc === "tasks" ? 600 : 500,
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.15s ease",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListTodo size={16} /> Execution Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT COLUMN: The Document Content */}
|
{/* RIGHT COLUMN: The Document Content */}
|
||||||
@@ -259,43 +270,122 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string,
|
|||||||
}}>
|
}}>
|
||||||
{activeDoc === "spec" && (
|
{activeDoc === "spec" && (
|
||||||
<div className="markdown-prose">
|
<div className="markdown-prose">
|
||||||
|
{plan.decisions?.find(d => d.id === "prd_spec")?.why ? (
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{plan.decisions.find(d => d.id === "prd_spec")!.why!}
|
||||||
|
</ReactMarkdown>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<h1>Product Specification</h1>
|
<h1>Product Specification</h1>
|
||||||
<p style={{ color: INK.muted }}>The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.</p>
|
<p style={{ color: INK.muted }}>The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.</p>
|
||||||
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
||||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
||||||
<BookOpen size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
<BookOpen size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
||||||
<p style={{ fontWeight: 500 }}>Spec is empty.</p>
|
<p style={{ fontWeight: 500 }}>Spec is empty.</p>
|
||||||
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to generate the Product Spec.</p>
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Click "Generate Complete PRD" on the Objective tab to generate this document.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeDoc === "plan" && (
|
{activeDoc === "plan" && (
|
||||||
<div className="markdown-prose">
|
<div className="markdown-prose">
|
||||||
|
{plan.decisions?.find(d => d.id === "prd_arch")?.why ? (
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{plan.decisions.find(d => d.id === "prd_arch")!.why!}
|
||||||
|
</ReactMarkdown>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<h1>Technical Architecture</h1>
|
<h1>Technical Architecture</h1>
|
||||||
<p style={{ color: INK.muted }}>The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.</p>
|
<p style={{ color: INK.muted }}>The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.</p>
|
||||||
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
||||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
||||||
<GitBranch size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
<GitBranch size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
||||||
<p style={{ fontWeight: 500 }}>Plan is empty.</p>
|
<p style={{ fontWeight: 500 }}>Plan is empty.</p>
|
||||||
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to define the technical approach.</p>
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Click "Generate Complete PRD" on the Objective tab to generate this document.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeDoc === "tasks" && (
|
|
||||||
<div className="markdown-prose">
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
||||||
<h1>Execution Plan</h1>
|
|
||||||
<button className="btn-primary">Delegate Build</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p style={{ color: INK.muted }}>The atomic, dependency-ordered tasks the AI will execute to build the application.</p>
|
</div>
|
||||||
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
);
|
||||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
}
|
||||||
<ListTodo size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
|
||||||
|
// ──────────────────────────────────────────────────
|
||||||
|
// 3. DELEGATE VIEW (The Factory)
|
||||||
|
// ──────────────────────────────────────────────────
|
||||||
|
function DelegateView({ plan, projectId, onChange }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
||||||
|
const [delegating, setDelegating] = useState(false);
|
||||||
|
const openTasks = plan.tasks.filter(t => t.status === "open");
|
||||||
|
|
||||||
|
const handleDelegate = async () => {
|
||||||
|
if (!confirm("Start the background runner to build this feature? You can safely close your browser while it works.")) return;
|
||||||
|
setDelegating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/projects/${projectId}/agent/sessions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
appName: "frontend",
|
||||||
|
appPath: ".",
|
||||||
|
task: "Execute all open tasks in the Execution Plan sequentially.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const err = await r.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || `HTTP ${r.status}`);
|
||||||
|
}
|
||||||
|
alert("Background runner started successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Failed to start runner: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
} finally {
|
||||||
|
setDelegating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel-container">
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>Execution Plan</h2>
|
||||||
|
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>The prioritized roadmap for the AI background runner to execute.</p>
|
||||||
|
</div>
|
||||||
|
<button className="btn-primary" onClick={handleDelegate} disabled={delegating || openTasks.length === 0}>
|
||||||
|
{delegating ? "Starting Jarvis..." : "Delegate Build"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "grid", gap: 12 }}>
|
||||||
|
{openTasks.length > 0 ? openTasks.map(t => (
|
||||||
|
<div key={t.id} style={{
|
||||||
|
border: `1px solid ${INK.border}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
background: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
|
<div style={{ width: 16, height: 16, borderRadius: "50%", border: `2px solid ${INK.muted}` }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: "0.95rem", color: INK.main }}>{t.title}</div>
|
||||||
|
{t.description && <div style={{ fontSize: "0.8rem", color: INK.muted, marginTop: 4 }}>{t.description}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: "0.8rem", color: INK.faint, background: INK.bgHover, padding: "4px 8px", borderRadius: 4 }}>Queued</span>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div style={{ padding: 40, textAlign: "center", border: `1px solid ${INK.border}`, borderRadius: 8, color: INK.muted }}>
|
||||||
|
<ListTodo size={32} style={{ margin: "0 auto 16px", opacity: 0.5 }} />
|
||||||
<p style={{ fontWeight: 500 }}>No tasks queued.</p>
|
<p style={{ fontWeight: 500 }}>No tasks queued.</p>
|
||||||
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Generate the task breakdown from the Product Spec.</p>
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px auto 0" }}>Use Architect mode to generate the task breakdown.</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { authSession } from "@/lib/auth/session-server";
|
||||||
|
import { query, queryOne } from "@/lib/db-postgres";
|
||||||
|
import { callVibnChat } from "@/lib/ai/vibn-chat-model";
|
||||||
|
|
||||||
|
// Generate a complete PRD, Tech Architecture, and Execution Plan based on the objective
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
req: Request,
|
||||||
|
ctx: { params: Promise<{ projectId: string }> }
|
||||||
|
) {
|
||||||
|
const { projectId } = await ctx.params;
|
||||||
|
const session = await authSession();
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRow = await queryOne<{ data: any; slug: string }>(
|
||||||
|
`SELECT data, slug FROM fs_projects
|
||||||
|
JOIN fs_users u ON u.id = fs_projects.user_id
|
||||||
|
WHERE fs_projects.id = $1 AND u.data->>'email' = $2 LIMIT 1`,
|
||||||
|
[projectId, session.user.email]
|
||||||
|
);
|
||||||
|
if (!projectRow) return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const body = await req.json().catch(() => ({}));
|
||||||
|
const objective = String(body.objective || projectRow.data?.plan?.vision || "").trim();
|
||||||
|
|
||||||
|
if (!objective) {
|
||||||
|
return NextResponse.json({ error: "Objective is required to generate a PRD." }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// We run three sequential LLM calls to construct the complete Spec Kit documentation
|
||||||
|
|
||||||
|
// 1. Generate the Product Spec (User Stories & Requirements)
|
||||||
|
const specPrompt = `You are a Principal Product Manager writing a Product Requirements Document (PRD).
|
||||||
|
Based on the following Objective, generate a structured PRD using Markdown.
|
||||||
|
DO NOT talk about technology (databases, frameworks, APIs). Focus purely on user value and capabilities.
|
||||||
|
|
||||||
|
OBJECTIVE:
|
||||||
|
${objective}
|
||||||
|
|
||||||
|
You MUST follow this exact structure:
|
||||||
|
|
||||||
|
## User Scenarios & Testing
|
||||||
|
List the 3-5 most critical User Stories, prioritized.
|
||||||
|
### User Story 1 - [Brief Title] (Priority: P1)
|
||||||
|
[Description]
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** [state], **When** [action], **Then** [outcome]
|
||||||
|
|
||||||
|
### User Story 2 - [Brief Title] (Priority: P2)
|
||||||
|
[Repeat...]
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
- **FR-001**: System MUST [capability]
|
||||||
|
- **FR-002**: Users MUST be able to [interaction]
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- **SC-001**: [Measurable business outcome]`;
|
||||||
|
|
||||||
|
const specResponse = await callVibnChat({
|
||||||
|
systemPrompt: specPrompt,
|
||||||
|
messages: [{ role: "user", content: "Generate the PRD." }],
|
||||||
|
temperature: 0.2
|
||||||
|
});
|
||||||
|
const specMd = specResponse.text || "Failed to generate PRD.";
|
||||||
|
|
||||||
|
// 2. Generate the Technical Architecture (Plan)
|
||||||
|
const planPrompt = `You are a Principal Software Architect.
|
||||||
|
Based on the following PRD, define the Technical Architecture required to build it.
|
||||||
|
Your choices must default to: Next.js (App Router), Postgres, Tailwind CSS, NextAuth, and Stripe (if payments).
|
||||||
|
|
||||||
|
PRD:
|
||||||
|
${specMd}
|
||||||
|
|
||||||
|
You MUST follow this exact structure:
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
- **Language/Framework**: Next.js 15 (App Router), TypeScript
|
||||||
|
- **Database**: Postgres (via Prisma or raw SQL)
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **Authentication**: [Decide auth strategy]
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
List the core database tables and their relationships.
|
||||||
|
- **[EntityName]**: [fields...]
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
Provide a high-level file tree representing the required Next.js pages and API routes.
|
||||||
|
\`\`\`text
|
||||||
|
src/app/
|
||||||
|
page.tsx
|
||||||
|
api/
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const planResponse = await callVibnChat({
|
||||||
|
systemPrompt: planPrompt,
|
||||||
|
messages: [{ role: "user", content: "Generate the Technical Architecture." }],
|
||||||
|
temperature: 0.1
|
||||||
|
});
|
||||||
|
const planMd = planResponse.text || "Failed to generate Architecture.";
|
||||||
|
|
||||||
|
// 3. Generate the Execution Plan (Tasks)
|
||||||
|
const tasksPrompt = `You are a Technical Project Manager.
|
||||||
|
Based on the PRD and Technical Architecture, break the implementation down into an atomic, dependency-ordered Task List.
|
||||||
|
You MUST format each task EXACTLY as a markdown bullet with a checkbox, a phase tag, and a description containing the file paths.
|
||||||
|
|
||||||
|
PRD:
|
||||||
|
${specMd}
|
||||||
|
|
||||||
|
ARCHITECTURE:
|
||||||
|
${planMd}
|
||||||
|
|
||||||
|
You MUST follow this exact structure:
|
||||||
|
|
||||||
|
## Phase 1: Setup & Foundation
|
||||||
|
- [ ] [Phase 1] Initialize Next.js project and layout
|
||||||
|
- [ ] [Phase 1] Setup Postgres database schema
|
||||||
|
|
||||||
|
## Phase 2: [User Story 1 Title]
|
||||||
|
- [ ] [US1] Build API route for [feature]
|
||||||
|
- [ ] [US1] Build frontend UI component in src/app/[route]/page.tsx
|
||||||
|
|
||||||
|
## Phase 3: [User Story 2 Title]
|
||||||
|
- [ ] [US2] ...`;
|
||||||
|
|
||||||
|
const tasksResponse = await callVibnChat({
|
||||||
|
systemPrompt: tasksPrompt,
|
||||||
|
messages: [{ role: "user", content: "Generate the Execution Plan tasks." }],
|
||||||
|
temperature: 0.1
|
||||||
|
});
|
||||||
|
const tasksMd = tasksResponse.text || "";
|
||||||
|
|
||||||
|
// 4. Parse the tasks string into the JSON array expected by the DB
|
||||||
|
// Extract all `- [ ] [Group] Task description` lines
|
||||||
|
const taskLines = tasksMd.match(/- \[[ x]\] \[(.*?)\] (.*)/g) || [];
|
||||||
|
const parsedTasks = taskLines.map(line => {
|
||||||
|
const match = line.match(/- \[[ x]\] \[(.*?)\] (.*)/);
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
id: Math.random().toString(36).slice(2, 11),
|
||||||
|
title: `[${match[1]}] ${match[2]}`, // Fixed backticks
|
||||||
|
description: "",
|
||||||
|
status: "open",
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
// 5. Update the Database Plan
|
||||||
|
const currentPlan = projectRow.data?.plan || {};
|
||||||
|
|
||||||
|
// Save the PRD under decisions (we can render it specially in the UI)
|
||||||
|
const newDecisions = [
|
||||||
|
{ id: "prd_spec", title: "Product Specification", choice: "Auto-generated PRD", why: specMd },
|
||||||
|
{ id: "prd_arch", title: "Technical Architecture", choice: "Auto-generated Plan", why: planMd }
|
||||||
|
];
|
||||||
|
|
||||||
|
const updatedPlan = {
|
||||||
|
...currentPlan,
|
||||||
|
vision: objective,
|
||||||
|
tasks: parsedTasks.length > 0 ? parsedTasks : currentPlan.tasks,
|
||||||
|
decisions: newDecisions
|
||||||
|
};
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`UPDATE fs_projects SET data = jsonb_set(data, '{plan}', $2::jsonb) WHERE id = $1`,
|
||||||
|
[projectId, JSON.stringify(updatedPlan)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({ plan: updatedPlan });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user