Files
Socrates/apps/web/components/editor/sections/FindingsPane.tsx
dtoro b55425cc68 Pivot to text-first column-stack workspace + merge-with-review across AI artifacts
Workspace
- Pivot from "set of open panes" to a Finder-style miller column stack:
  TopBar / LeftSidebar / [section → entity → entity ...] / pinned text editor.
  openPanesStore is now an ordered Column[] with pushFrom / closeFrom /
  setStack; only one top-level section is rooted at a time.
- New entity column panes: Term, Block, Association, Constraint,
  Requirement, Finding. Click-through navigation truncates deeper
  columns automatically.
- LeftSidebar surfaces a pending-count chip per section (single-glance
  navigation cue) and spins its analyze ↻ via SVG Spinner whenever the
  LLM is working — including server-initiated runs caught by the runs
  poll, not just user-triggered ones.

Analyze pipeline + persistence
- Unified `concepts` pass (taxonomy + glossary in one LLM call) replaces
  the two-pass setup. Server still accepts ?section=taxonomy|glossary
  and normalizes them for back-compat.
- model / requirements / detection (assumptions, risks, inconsistencies)
  + cross-layer validation rules (X1–X4: stale term link, unlinked
  formalism, undefined linked term, prose-only term).
- Persistence: NarrativeDocument, ModelSnapshot, ChangelogEntry,
  TaxonomyTerm, RequirementEntry, Finding, AnalysisRun. Re-runs MERGE
  instead of replace: gentle update on existing items, suggested on new,
  deprecated on missing — same idiom for every artifact kind. User pins
  preserve "kept" decisions across re-analyses.
- Migrations: pivot_text_first, add_requirement_linked_term,
  term_review_state, review_state_for_reqs_and_findings,
  add_term_definition_pinned.

Concept ↔ ontology integration
- linkedTermId on Block / Association / Constraint / Requirement.
  PromoteToolbar lets the user formalize a concept inline: + Block /
  + Association / + Constraint / + Requirement, all routed through
  applyOps so undo/redo and SSE work for free.
- decideElement op for in-canvas keep/discard on review-pending model
  elements.

User-authored definitions
- TermColumn definition is click-to-edit. Save (Cmd-Enter / blur),
  Cancel (Esc), Reset to AI suggestion when pinned.
- definitionPinned flag on TaxonomyTerm: future Analyze runs leave the
  user's text alone. setTermDefinition repo function + POST
  /api/projects/[id]/terms/[termId]/definition endpoint.
- mergeTaxonomySuggestion + applyGlossaryDefinitions both pin-aware.

UX/UI
- StatusChip: single component for all state idioms (suggested,
  deprecated, accepted, dismissed, resolved, severity, validation code,
  confidence, warn). Replaces 5+ ad-hoc badge classes.
- PaneControls (PaneViewTabs + PaneFilterChip): separates view-mode
  toggles from filter chips so toggling Pending no longer flips you off
  the current view.
- PaneEmpty: unified empty-state with title + hint + action.
- PaneDrawer: collapsible groups for Pending / Discarded review; cards
  group as Kept (top) → Pending (bottom drawer) → Discarded (Findings
  only, hidden when empty). Restore action recovers dismissed/resolved
  findings.
- ConceptCard unifies Tree and A–Z views in Concepts; only Tree parents
  carry the chevron (no empty placeholder offset).
- Type + spacing tokens (--text-xs..xl, --space-1..6, --lh-tight/ui/
  prose, --radius-*) replace every ad-hoc value.
- Buttons standardized to body sans 500 (was a mishmash of mono / display).
- Card shells unified across Concepts / Requirements / Findings.

Cleanup
- Removed: LeftRail, FindingsPanel, IssuesPanel, SocratesDock,
  ProposalCard, SlashMenu, SlashExtension, slashSuggestion,
  CanvasHeader, TaxonomyPane, GlossaryPane, TermDetail (popover; now
  TermColumn).
- Section ids in openPanesStore: dropped taxonomy/glossary, added
  concepts. localStorage migration runs on hydrate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:12:06 +02:00

232 lines
7.3 KiB
TypeScript

// Findings pane — three groups of rows for one kind (assumption / risk /
// inconsistency):
//
// 1. Kept — status="accepted". Top of the pane, scrollable.
// 2. Pending — status in {suggested, deprecated}. Drawer below kept.
// 3. Discarded — status in {dismissed, resolved}. Collapsed drawer at the
// bottom, hidden when empty.
//
// Clicking a row pushes a FindingColumn to the right (full detail + Socrates
// thread + decision actions). Decisions also work inline from the row.
"use client";
import { useMemo } from "react";
import { PaneFrame } from "../PaneFrame";
import { PaneEmpty } from "../PaneEmpty";
import { PaneDrawer } from "../PaneDrawer";
import { StatusChip } from "../StatusChip";
import { useAnalysis, type ClientFinding } from "../../../lib/workspace/analysisStore";
import { useOpenPanes } from "../../../lib/workspace/openPanesStore";
interface FindingsPaneProps {
kind: "assumption" | "risk" | "inconsistency";
index: number;
projectId: string;
onClose: () => void;
}
const TITLES: Record<FindingsPaneProps["kind"], string> = {
assumption: "Assumptions",
risk: "Risks",
inconsistency: "Inconsistencies",
};
const SECTION_TO_ANALYZE = {
assumption: "assumptions",
risk: "risks",
inconsistency: "inconsistencies",
} as const;
const PENDING_STATUSES = new Set(["suggested", "deprecated"]);
const DISCARDED_STATUSES = new Set(["dismissed", "resolved"]);
export function FindingsPane({ kind, index, onClose }: FindingsPaneProps) {
const { findings, analyze, inFlight } = useAnalysis();
const { pushFrom, columns } = useOpenPanes();
const items = useMemo(() => findings.filter(f => f.kind === kind), [findings, kind]);
const kept = useMemo(() => items.filter(f => f.status === "accepted"), [items]);
const pending = useMemo(() => items.filter(f => PENDING_STATUSES.has(f.status)), [items]);
const discarded = useMemo(() => items.filter(f => DISCARDED_STATUSES.has(f.status)), [items]);
const section = SECTION_TO_ANALYZE[kind];
const running = inFlight.has(section) || inFlight.has("all");
const activeId =
columns[index + 1]?.kind === "finding" ? columns[index + 1]!.id : null;
const subtitle =
pending.length > 0
? `${kept.length} kept · ${pending.length} pending`
: `${kept.length} kept`;
const right = (
<div className="pane-controls">
<button
type="button"
className="pane-action"
onClick={() => void analyze(section)}
disabled={running}
>
{running ? "Analyzing…" : "Analyze"}
</button>
</div>
);
const renderRow = (f: ClientFinding) => (
<FindingRow
key={f.id}
finding={f}
active={activeId === f.id}
onOpen={() => pushFrom(index, { kind: "finding", id: f.id })}
/>
);
return (
<PaneFrame title={TITLES[kind]} subtitle={subtitle} right={right} onClose={onClose}>
<div className="pane-drawer-stack">
<div className="pane-drawer-main">
{items.length === 0 ? (
<PaneEmpty
title={`No ${TITLES[kind].toLowerCase()} yet`}
hint={`${TITLES[kind]} are detected against the current model. Run Analyze to scan it for issues.`}
action={{
label: running ? "Analyzing…" : "Run Analyze →",
onClick: () => void analyze(section),
disabled: running,
}}
/>
) : kept.length === 0 ? (
<PaneEmpty
title="Nothing kept yet"
hint="Review the pending findings below to start tracking the ones that matter."
/>
) : (
<ul className="finding-list">{kept.map(renderRow)}</ul>
)}
</div>
{pending.length > 0 ? (
<PaneDrawer title="Pending review" count={pending.length} tone="pending" defaultOpen>
<ul className="finding-list">{pending.map(renderRow)}</ul>
</PaneDrawer>
) : null}
<PaneDrawer
title="Discarded"
count={discarded.length}
tone="muted"
hideWhenEmpty
>
<ul className="finding-list">{discarded.map(renderRow)}</ul>
</PaneDrawer>
</div>
</PaneFrame>
);
}
function FindingRow({
finding,
active,
onOpen,
}: {
finding: ClientFinding;
active: boolean;
onOpen: () => void;
}) {
const { decideFinding } = useAnalysis();
const isPending = PENDING_STATUSES.has(finding.status);
const isDiscarded = DISCARDED_STATUSES.has(finding.status);
return (
<li
className={`finding-row finding-status-${finding.status} ${active ? "row-active" : ""}`}
onClick={onOpen}
role="button"
tabIndex={0}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onOpen();
}
}}
>
<div className="finding-row-head finding-row-head-static">
<span className="finding-text">{finding.text}</span>
<span className="finding-meta">
{finding.status === "suggested" ? <StatusChip variant="suggested" /> : null}
{finding.status === "deprecated" ? <StatusChip variant="deprecated" /> : null}
{finding.status === "dismissed" ? <StatusChip variant="dismissed" /> : null}
{finding.status === "resolved" ? <StatusChip variant="resolved" /> : null}
{finding.validationCode ? (
<StatusChip variant="code" value={finding.validationCode} title="Validation rule" />
) : null}
{finding.severity ? (
<StatusChip
variant="severity"
value={finding.severity as "low" | "medium" | "high"}
title="Severity"
/>
) : null}
<StatusChip
variant="confidence"
value={finding.confidence}
title={`${Math.round(finding.confidence * 100)}% confidence`}
/>
</span>
</div>
{isPending ? (
<div className="term-decision-row" onClick={e => e.stopPropagation()}>
<button
type="button"
className="term-decide term-decide-keep"
onClick={e => {
e.stopPropagation();
void decideFinding(finding.id, "keep");
}}
>
Keep
</button>
<button
type="button"
className="term-decide term-decide-discard"
onClick={e => {
e.stopPropagation();
void decideFinding(finding.id, "discard");
}}
>
Dismiss
</button>
{finding.status === "suggested" ? (
<button
type="button"
className="term-decide term-decide-resolve"
onClick={e => {
e.stopPropagation();
void decideFinding(finding.id, "resolve");
}}
>
Resolve
</button>
) : null}
</div>
) : isDiscarded ? (
<div className="term-decision-row" onClick={e => e.stopPropagation()}>
<button
type="button"
className="term-decide term-decide-keep"
onClick={e => {
e.stopPropagation();
void decideFinding(finding.id, "restore");
}}
title="Move back to Kept"
>
Restore
</button>
</div>
) : null}
</li>
);
}