"use client"; /** * Plan tab — the home of the project's thinking. * * Sub-areas (always visible, even when empty, so a founder learns the * model on a brand-new project): * 1. Vision — the elevator pitch + audience * 2. Ideas — the "park-it" bin for raw thoughts * 3. Tasks — what needs to happen next (open / done) * 4. Decisions — log of "we chose X over Y because Z" * * Same UI rhythm as Infrastructure: section tiles in a horizontal bar * (with counts), full-width detail panel below. Everything writes through * `/api/projects/[projectId]/plan` and persists under * `fs_projects.data.plan`. The AI chat reads the plan as context so * decisions don't get re-litigated. */ import { useCallback, useEffect, useState } from "react"; import { useParams } from "next/navigation"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Loader2, AlertCircle, MessageSquare, ListTodo, GitBranch, Target, Plus, Trash2, Check, RotateCcw, Pencil, X, Eye, FileText, } from "lucide-react"; interface Idea { id: string; text: string; createdAt: string; } type TaskStatus = "open" | "in_progress" | "review" | "done" | "blocked"; interface Task { id: string; title: string; description?: string; status: TaskStatus; agent?: { runId: string; startedAt: string; finishedAt?: string; status: "queued" | "running" | "succeeded" | "failed"; } | null; createdAt: string; startedAt?: string; doneAt?: string; // Legacy single-line tasks (pre-markdown migration) carry text instead of title. text?: string; } interface Decision { id: string; title: string; choice: string; why?: string; createdAt: string; } interface Plan { vision?: string; brief?: string; brief_filename?: string; // stored as `vision` server-side, surfaced as "Objective" in the UI ideas: Idea[]; // legacy bin — no longer surfaced; kept on the model for backward compat tasks: Task[]; decisions: Decision[]; } interface Session { id: string; title: string; summary: string | null; messageCount: number; updatedAt: string; createdAt: string; } type Section = "objective" | "sessions" | "tasks" | "decisions"; interface SectionDef { key: Section; label: string; icon: React.ComponentType<{ size?: number; style?: React.CSSProperties }>; blurb: string; } const SECTIONS: SectionDef[] = [ { key: "objective", label: "Objective", icon: Target, blurb: "What you're building, who it's for, and why.", }, { key: "tasks", label: "Tasks", icon: ListTodo, blurb: "What needs to happen next.", }, { key: "decisions", label: "Decisions", icon: GitBranch, blurb: "Choices already made — so we stop re-litigating them.", }, { key: "sessions", label: "Sessions", icon: MessageSquare, blurb: "Past chat sessions for this project.", }, ]; // ────────────────────────────────────────────────── // Page // ────────────────────────────────────────────────── export default function PlanTab() { const params = useParams(); const projectId = params.projectId as string; const workspace = params.workspace as string; const [plan, setPlan] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [active, setActive] = useState
("objective"); const [sessionCount, setSessionCount] = useState(0); 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(); const intervalId = setInterval(load, 5000); return () => clearInterval(intervalId); }, [load]); if (loading && !plan) { return (
Loading plan…
); } if (error || !plan) { return (
{error ?? "No plan data"}
); } const counts: Record = { objective: plan.vision ? 1 : 0, sessions: sessionCount, tasks: plan.tasks.filter((t) => t.status !== "done").length, decisions: plan.decisions.length, }; return (
{active === "objective" && ( )} {active === "sessions" && ( )} {active === "tasks" && ( )} {active === "decisions" && ( )}
); } // ────────────────────────────────────────────────── // Left rail tile // ────────────────────────────────────────────────── function SectionTile({ def, count, active, onClick, }: { def: SectionDef; count: number; active: boolean; onClick: () => void; }) { const Icon = def.icon; return ( ); } // ────────────────────────────────────────────────── // Vision // ────────────────────────────────────────────────── function BriefPanel({ plan, projectId, onChange, }: { plan: Plan; projectId: string; onChange: (p: Plan) => void; }) { const [draft, setDraft] = useState(plan.brief ?? ""); const [saving, setSaving] = useState(false); const [editing, setEditing] = useState(!plan.brief); const [editorView, setEditorView] = useState<"write" | "preview">("write"); const [dirty, setDirty] = useState(false); useEffect(() => { setDraft(plan.brief ?? ""); setDirty(false); }, [plan.brief]); const handleSave = async () => { setSaving(true); try { const res = await fetch(`/api/projects/${projectId}/plan`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "brief", text: draft }), }); if (res.ok) { onChange({ ...plan, brief: draft }); setDirty(false); setEditing(false); } } finally { setSaving(false); } }; const handleFileUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // For now we just read text files (txt, md, csv). // In the future you can send PDFs to an extraction API. const reader = new FileReader(); reader.onload = async (event) => { const content = event.target?.result as string; setDraft(content); setDirty(true); setEditing(true); }; reader.readAsText(file); }; if (!editing && plan.brief) { return (
(

), h1: ({ node, ...props }) => (

), h2: ({ node, ...props }) => (

), h3: ({ node, ...props }) => (

), ul: ({ node, ...props }) => (
    ), ol: ({ node, ...props }) => (
      ), li: ({ node, ...props }) => (
    1. ), }} > {plan.brief}

); } return (
{!plan.brief && (

No project brief uploaded. You can paste your PRD, requirements document, or notes below. The AI will read this to understand the full scope of your project.

)} {editing && (
{editorView === "write" ? (