// 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}
{sections.map(s => (
))}
);
}
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 "";
}