// Model pane — fully editable React Flow diagram + a Summary subtab listing // blocks/associations/constraints/requirements with a click-to-jump UX + // review controls (Keep / Discard) on analyzer-suggested or analyzer-deprecated // elements. "use client"; import { useState } from "react"; import { PaneFrame } from "../PaneFrame"; import { PaneViewTabs, PaneFilterChip } from "../PaneControls"; import { StatusChip } from "../StatusChip"; import { DiagramCanvas } from "../../diagram-canvas/DiagramCanvas"; import { useModelStore } from "../../../lib/sync/ModelStore"; import { useAnalysis } from "../../../lib/workspace/analysisStore"; import { useEditorPaneContext } from "./paneContext"; import { useOpenPanes } from "../../../lib/workspace/openPanesStore"; import { decideElement, removeBlock, removeAssociation, removeConstraint, removeRequirement, type ReviewableElementKind, } from "../../../lib/sync/ops"; import type { ReviewStatus, SysMLModel } from "../../../lib/sysml/model"; interface ModelPaneProps { index: number; onClose: () => void; } type Tab = "diagram" | "summary"; export function ModelPane({ index, onClose }: ModelPaneProps) { const [tab, setTab] = useState("diagram"); const [pendingOnly, setPendingOnly] = useState(false); const { model, issuesByElement } = useModelStore(); const { analyze, inFlight } = useAnalysis(); const { focusBlockId, setFocusBlockId, projectId } = useEditorPaneContext(); const running = inFlight.has("model") || inFlight.has("all"); const pendingCount = countPending(model); const right = (
value={tab} onChange={setTab} ariaLabel="Model view mode" tabs={[ { value: "diagram", label: "Diagram" }, { value: "summary", label: "Summary" }, ]} /> { // Pending now overlays the current view mode rather than replacing // it. If we're on Diagram and the user wants to triage pending // items, switching to Summary makes far more sense than rendering // a filtered diagram, so we still nudge to Summary on toggle-on. setPendingOnly(p => { if (!p && tab === "diagram") setTab("summary"); return !p; }); }} label="Pending" count={pendingCount} title="Show only suggestions and deprecated model elements" />
); const subtitle = pendingCount > 0 ? `${model.blocks.length} blocks · ${model.associations.length} assocs · ${model.constraints.length} constraints · ${pendingCount} pending` : `${model.blocks.length} blocks · ${model.associations.length} assocs · ${model.constraints.length} constraints`; return ( {tab === "diagram" && !pendingOnly ? (
) : ( )}
); } function countPending(model: SysMLModel): number { const isPending = (rs?: ReviewStatus) => rs === "suggested" || rs === "deprecated"; return ( model.blocks.filter(b => isPending(b.reviewStatus)).length + model.associations.filter(a => isPending(a.reviewStatus)).length + model.constraints.filter(c => isPending(c.reviewStatus)).length + model.requirements.filter(r => isPending(r.reviewStatus)).length ); } function ModelSummary({ pendingOnly, parentIndex }: { pendingOnly: boolean; parentIndex: number }) { const { model, apply } = useModelStore(); const { pushFrom } = useOpenPanes(); const filterPending = (xs: T[]): T[] => pendingOnly ? xs.filter(x => x.reviewStatus === "suggested" || x.reviewStatus === "deprecated") : xs; const blocks = filterPending(model.blocks); const associations = filterPending(model.associations); const constraints = filterPending(model.constraints); const requirements = filterPending(model.requirements); const totalShown = blocks.length + associations.length + constraints.length + requirements.length; if (totalShown === 0) { return (
{pendingOnly ? ( <>No pending changes. Everything is up to date. ) : ( <> No model yet. Click Analyze to derive one from your prose, or open the diagram and start dragging. )}
); } const decideKeep = (kind: ReviewableElementKind, id: string) => apply([decideElement({ kind, id })]); const decideDiscard = (kind: ReviewableElementKind, id: string) => { if (kind === "block") apply([removeBlock(id)]); else if (kind === "association") apply([removeAssociation(id)]); else if (kind === "constraint") apply([removeConstraint(id)]); else if (kind === "requirement") apply([removeRequirement(id)]); }; return (
{blocks.length > 0 && ( <>

Blocks ({blocks.length})

    {blocks.map(b => (
  • {b.linkedTermId ? ( ) : null} decideKeep("block", b.id)} onDiscard={() => decideDiscard("block", b.id)} />
  • ))}
)} {associations.length > 0 && ( <>

Associations ({associations.length})

    {associations.map(a => (
  • decideKeep("association", a.id)} onDiscard={() => decideDiscard("association", a.id)} />
  • ))}
)} {constraints.length > 0 && ( <>

Constraints ({constraints.length})

    {constraints.map(c => (
  • decideKeep("constraint", c.id)} onDiscard={() => decideDiscard("constraint", c.id)} />
  • ))}
)} {requirements.length > 0 && ( <>

Requirements ({requirements.length})

    {requirements.map(r => (
  • decideKeep("requirement", r.id)} onDiscard={() => decideDiscard("requirement", r.id)} />
  • ))}
)}
); } function ReviewRow({ status, children, }: { status?: ReviewStatus; children: React.ReactNode; }) { return (
{status === "suggested" ? : null} {status === "deprecated" ? : null} {children}
); } function DecideButtons({ status, onKeep, onDiscard, }: { status?: ReviewStatus; onKeep: () => void; onDiscard: () => void; }) { if (status !== "suggested" && status !== "deprecated") return null; return ( ); } function labelOf(model: SysMLModel, id: string): string { return model.blocks.find(b => b.id === id)?.label ?? id; }