"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useParams } from "next/navigation"; import { Loader2, Target, BookOpen, Layers, GitBranch, Pencil, FileText, Check, Plus } from "lucide-react"; import ReactMarkdown from "react-markdown"; // 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" | "stories" | "features" | "architecture"; // Shared Theme Variables const INK = { main: "#1a1918", muted: "#6b6560", faint: "#a09a90", border: "#e8e4dc", bg: "#ffffff", bgHover: "#f8f6f2", }; export default function PlanPageV2() { const params = useParams(); const projectId = (params?.projectId as string) || ""; const [plan, setPlan] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("objective"); const load = useCallback(async () => { try { const res = await fetch(`/api/projects/${projectId}/plan`, { credentials: "include" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setPlan(data.plan); setError(null); } catch (e: any) { setError(e?.message ?? "Failed to load plan"); } finally { setLoading(false); } }, [projectId]); useEffect(() => { load(); // Safe polling interval const intervalId = setInterval(load, 15000); return () => clearInterval(intervalId); }, [load]); if (loading && !plan) { return (
); } if (error || !plan) { return (
{error || "Failed to load plan"}
); } return (
{/* ── HEADER & TABS ── */}
setActiveTab("objective")} icon={} label="Objective" /> setActiveTab("stories")} icon={} label="User Stories" /> setActiveTab("features")} icon={} label="Features" /> setActiveTab("architecture")} icon={} label="Architecture" />
{/* ── 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 [editing, setEditing] = useState(!plan.vision); const [draft, setDraft] = useState(plan.vision ?? ""); const [saving, setSaving] = useState(false); const save = async () => { setSaving(true); try { const r = await fetch(`/api/projects/${projectId}/plan`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "vision", text: draft }), }); const d = await r.json(); if (d.plan) onChange(d.plan); setEditing(false); } finally { setSaving(false); } }; return (
{!editing && ( )}
{editing ? (