"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { JM, JV } from "@/components/project-creation/modal-theme"; import { PRD_PLAN_SECTIONS, isSectionFilled } from "@/lib/prd-sections"; import { type ChatContextRef, contextRefKey, } from "@/lib/chat-context-refs"; interface SavedPhase { phase: string; title: string; summary: string; data: Record; saved_at: string; } export function BuildLivePlanPanel({ projectId, workspace, chatContextRefs, onAddSectionRef, compactHeader, }: { projectId: string; workspace: string; chatContextRefs: ChatContextRef[]; onAddSectionRef: (label: string, phaseId: string | null) => void; /** When true, hide subtitle to save space in narrow tabs */ compactHeader?: boolean; }) { const [prdText, setPrdText] = useState(null); const [savedPhases, setSavedPhases] = useState([]); const [loading, setLoading] = useState(true); const refresh = useCallback(() => { Promise.all([ fetch(`/api/projects/${projectId}`).then(r => r.json()).catch(() => ({})), fetch(`/api/projects/${projectId}/save-phase`).then(r => r.json()).catch(() => ({ phases: [] })), ]).then(([projectData, phaseData]) => { setPrdText(projectData?.project?.prd ?? null); setSavedPhases(phaseData?.phases ?? []); setLoading(false); }); }, [projectId]); useEffect(() => { refresh(); const t = setInterval(refresh, 8000); return () => clearInterval(t); }, [refresh]); const savedPhaseIds = useMemo(() => new Set(savedPhases.map(p => p.phase)), [savedPhases]); const phaseMap = useMemo(() => new Map(savedPhases.map(p => [p.phase, p])), [savedPhases]); const rows = useMemo(() => { let firstOpenIndex = -1; const list = PRD_PLAN_SECTIONS.map((s, index) => { const done = isSectionFilled(s.phaseId, savedPhaseIds); if (!done && firstOpenIndex < 0) firstOpenIndex = index; return { ...s, done, active: !done && index === firstOpenIndex, pending: !done && index > firstOpenIndex, savedPhase: s.phaseId ? phaseMap.get(s.phaseId) ?? null : null, }; }); return list; }, [savedPhaseIds, phaseMap]); const doneCount = rows.filter(r => r.done).length; const tasksHref = `/${workspace}/project/${projectId}/tasks`; const attached = useCallback( (label: string) => chatContextRefs.some(r => r.kind === "section" && r.label === label), [chatContextRefs] ); if (loading) { return (
Loading plan…
); } if (prdText) { return (
Your plan
PRD ready
{!compactHeader && (
Full document saved — open Task to edit or keep refining in chat.
)}
View full PRD →
); } return (
Your plan
Fills as you chat
{!compactHeader && (
Tap a section to attach it — your next message prioritizes it.
)}
{Math.round((doneCount / rows.length) * 100)}%
{doneCount}/{rows.length}
{rows.map((r, i) => { const isAttached = attached(r.label); const hint = r.done && r.savedPhase?.summary ? r.savedPhase.summary.slice(0, 120) + (r.savedPhase.summary.length > 120 ? "…" : "") : r.active ? "Answer in chat — this block updates when the phase saves." : "Tap to attach — chat uses this section as context."; return ( ); })}
Open requirements view
); } export function addSectionContextRef( prev: ChatContextRef[], label: string, phaseId: string | null ): ChatContextRef[] { const next: ChatContextRef = { kind: "section", label, phaseId }; const k = contextRefKey(next); if (prev.some(r => contextRefKey(r) === k)) return prev; return [...prev, next]; }