"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useParams } from "next/navigation"; import { Loader2, Target, BookOpen, Layers, GitBranch, Pencil, FileText, Check, Plus, ListTodo } from "lucide-react"; import ReactMarkdown from "react-markdown"; import useSWR from "swr"; // Types mapping to our Postgres plan shape type Plan = { vision?: string; brief?: string; tasks: Array<{ id: string; title: string; description?: string; status: string }>; decisions: Array<{ id: string; title: string; choice: string; why?: string }>; }; type Tab = "objective" | "prd" | "delegate"; // Shared Theme Variables const INK = { main: "#1a1918", muted: "#6b6560", faint: "#a09a90", border: "#e8e4dc", bg: "#ffffff", bgHover: "#f8f6f2", }; const fetcher = async (url: string) => { const res = await fetch(url, { credentials: "include" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return data.plan; }; export default function PlanPageV2() { const params = useParams(); const projectId = (params?.projectId as string) || ""; const [activeTab, setActiveTab] = useState("objective"); const { data: plan, error, mutate: mutatePlan } = useSWR( projectId ? `/api/projects/${projectId}/plan` : null, fetcher, { revalidateOnFocus: false, revalidateIfStale: false } ); // Wrapper for child components to update SWR cache optimally const setPlan = (newPlan: Plan) => { mutatePlan(newPlan, false); }; if (!plan && !error) { return (
); } if (error || !plan) { return (
{error || "Failed to load plan"}
); } return (
{/* ── HEADER & TABS ── */}
setActiveTab("objective")} icon={} label="Objective" /> setActiveTab("prd")} icon={} label="PRD" /> setActiveTab("delegate")} icon={} label="Delegate" />
{/* ── CONTENT AREA ── */} {/* By rendering them all but toggling display: none, we fix the "always needs to load" issue you mentioned. The DOM nodes stay mounted, preserving state and preventing jitter. */}
); } // ────────────────────────────────────────────────── // 1. OBJECTIVE VIEW // The business case / 1-page summary // ────────────────────────────────────────────────── function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) { const [draft, setDraft] = useState(plan.vision ?? ""); const [saving, setSaving] = useState(false); const [generating, setGenerating] = useState(false); useEffect(() => { if (plan.vision !== draft) { setDraft(plan.vision ?? ""); } }, [plan.vision]); const save = async (text: string) => { setSaving(true); try { const r = await fetch(`/api/projects/${projectId}/plan`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "vision", text }), }); const d = await r.json(); if (d.plan) onChange(d.plan); } finally { setSaving(false); } }; const handleGenerate = async () => { if (!draft.trim()) { alert("Please write an objective first before generating the PRD."); return; } if (!confirm("This will overwrite the PRD and Execution Plan based on this objective. Continue?")) return; setGenerating(true); try { await fetch(`/api/projects/${projectId}/plan`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "vision", text: draft }), }); 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); alert("Blueprint generated successfully! Check the PRD and Delegate tabs."); } finally { setGenerating(false); } }; return (
{saving && Saving...}