// Single sidebar for the pivoted shell. Lists every section grouped under // Structure / Findings. Each row shows: title, count, last-run timestamp, // per-section [Analyze] button, and is itself clickable to toggle the matching // pane open in the main workspace. "use client"; import { useOpenPanes, SECTION_TITLES, type SectionPaneId } from "../../lib/workspace/openPanesStore"; import { useAnalysis } from "../../lib/workspace/analysisStore"; import { Spinner } from "./Spinner"; import { useState } from "react"; const STRUCTURE: SectionPaneId[] = ["concepts", "model", "requirements"]; const FINDINGS: SectionPaneId[] = ["assumptions", "risks", "inconsistencies"]; export function LeftSidebar() { const [collapsed, setCollapsed] = useState(false); if (collapsed) { return ( ); } return ( ); } function Group({ label, sections }: { label: string; sections: SectionPaneId[] }) { return (
{label}
); } function SectionRow({ id }: { id: SectionPaneId }) { const { isSectionOpen, toggleSection } = useOpenPanes(); const { terms, requirements, findings, inFlight, runs, analyze } = useAnalysis(); const open = isSectionOpen(id); const count = sectionCount(id, { terms, requirements, findings }); const pending = pendingCount(id, { terms, requirements, findings }); // Spinner is on whenever: // - the user just clicked Analyze on this section (inFlight) // - the user clicked Analyze All // - the server has a "running" AnalysisRun for this section (catches // background runs like seed-finalize and the first-paint auto-analyze // that the local inFlight set doesn't know about). The analysisStore // polls `runs` every 3s while anything is running, so this stays // truthy until the run actually finishes. const running = inFlight.has(id) || inFlight.has("all") || (id === "concepts" && (inFlight.has("taxonomy") || inFlight.has("glossary"))) || runs[id]?.status === "running" || runs.all?.status === "running" || (id === "concepts" && (runs.taxonomy?.status === "running" || runs.glossary?.status === "running")); // ONE number per row: pending if there's review work, otherwise total. // Runs the user the most useful signal first ("how much attention does // this section need?") and avoids the duplicate-count effect when // pending === total because nothing is accepted yet. const showPending = pending > 0; return (
  • ); } function sectionCount( id: SectionPaneId, data: { terms: { id: string; status: string }[]; requirements: { id: string; status: string }[]; findings: { kind: string; status: string }[]; } ): number { if (id === "concepts") return data.terms.length; if (id === "model") return 0; // Model count is shown inside the pane itself. if (id === "requirements") return data.requirements.length; return data.findings.filter(f => f.kind === idToFindingKind(id)).length; } /** Count items the user hasn't reviewed yet (suggested + deprecated). The * most useful navigation cue we can put in the sidebar — tells the user * where attention is needed at a glance. */ function pendingCount( id: SectionPaneId, data: { terms: { status: string }[]; requirements: { status: string }[]; findings: { kind: string; status: string }[]; } ): number { const isPending = (s: string) => s === "suggested" || s === "deprecated"; if (id === "concepts") return data.terms.filter(t => isPending(t.status)).length; if (id === "requirements") return data.requirements.filter(r => isPending(r.status)).length; if (id === "model") return 0; // Model pending count needs ModelStore — surfaced in the pane header. return data.findings.filter(f => f.kind === idToFindingKind(id) && isPending(f.status)).length; } function idToFindingKind(id: SectionPaneId): string { if (id === "assumptions") return "assumption"; if (id === "risks") return "risk"; if (id === "inconsistencies") return "inconsistency"; return ""; }