From 94a6198db3a84937d23f448a212c5b3fe76e873c Mon Sep 17 00:00:00 2001 From: mawkone Date: Tue, 19 May 2026 18:48:35 -0700 Subject: [PATCH] fix(ui): simplify Plan tab by removing redundant Objective heading --- .../[projectId]/(home)/plan/page-v1.tsx | 2441 ++++++++++++++++ .../project/[projectId]/(home)/plan/page.tsx | 2571 ++--------------- vibn-frontend/fix_syntax5.js | 9 + 3 files changed, 2689 insertions(+), 2332 deletions(-) create mode 100644 vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx create mode 100644 vibn-frontend/fix_syntax5.js diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx new file mode 100644 index 00000000..d54fa053 --- /dev/null +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx @@ -0,0 +1,2441 @@ +"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: "Features", + icon: ListTodo, + blurb: "High-level capabilities to be delegated to the AI.", + }, + { + 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(); + // Only poll the plan API periodically if we aren't currently editing or saving + // something, otherwise we risk overwriting user input or causing layout thrashing. + const intervalId = setInterval(load, 15000); // Backed off from 5s to 15s to reduce API spam + 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" ? ( +