diff --git a/app/[workspace]/project/[projectId]/(home)/layout.tsx b/app/[workspace]/project/[projectId]/(home)/layout.tsx index 9a566fd9..0f19b631 100644 --- a/app/[workspace]/project/[projectId]/(home)/layout.tsx +++ b/app/[workspace]/project/[projectId]/(home)/layout.tsx @@ -6,6 +6,7 @@ import { ReactNode } from "react"; import { Toaster } from "sonner"; import { ChatPanel } from "@/components/vibn-chat/chat-panel"; +import { ProjectStreamHandler } from "@/components/project/project-stream-handler"; export default async function ProjectShell({ children, @@ -14,10 +15,11 @@ export default async function ProjectShell({ children: ReactNode; params: Promise<{ workspace: string; projectId: string }>; }) { - const { workspace } = await params; + const { workspace, projectId } = await params; return ( <> +
diff --git a/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx b/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx deleted file mode 100644 index d54fa053..00000000 --- a/app/[workspace]/project/[projectId]/(home)/plan/page-v1.tsx +++ /dev/null @@ -1,2441 +0,0 @@ -"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" ? ( -