Some checks failed
CI / check (push) Has been cancelled
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop. Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
8.2 KiB
TypeScript
233 lines
8.2 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { useDiagramStore } from "@/store/diagramStore";
|
||
import { buildDofCoach, type CoachSeverity } from "@/lib/dofCoach";
|
||
import type { DofBalance } from "@/lib/dofLedger";
|
||
import type { EntropykNodeData } from "@/lib/configBuilder";
|
||
import type { Node } from "@xyflow/react";
|
||
import ApiHealthIndicator from "@/components/shell/ApiHealthIndicator";
|
||
import {
|
||
AlertTriangle,
|
||
CheckCircle2,
|
||
ChevronUp,
|
||
Equal,
|
||
Lightbulb,
|
||
MinusCircle,
|
||
Sparkles,
|
||
} from "lucide-react";
|
||
|
||
function balanceStyle(balance: DofBalance): {
|
||
bg: string;
|
||
fg: string;
|
||
border: string;
|
||
Icon: typeof CheckCircle2;
|
||
} {
|
||
switch (balance) {
|
||
case "balanced":
|
||
return {
|
||
bg: "bg-emerald-50",
|
||
fg: "text-emerald-800",
|
||
border: "border-emerald-200",
|
||
Icon: CheckCircle2,
|
||
};
|
||
case "over-constrained":
|
||
return {
|
||
bg: "bg-red-50",
|
||
fg: "text-red-800",
|
||
border: "border-red-200",
|
||
Icon: AlertTriangle,
|
||
};
|
||
case "under-constrained":
|
||
return {
|
||
bg: "bg-amber-50",
|
||
fg: "text-amber-900",
|
||
border: "border-amber-200",
|
||
Icon: MinusCircle,
|
||
};
|
||
default:
|
||
return {
|
||
bg: "bg-[var(--chrome-2)]",
|
||
fg: "text-[var(--ink-dim)]",
|
||
border: "border-[var(--line)]",
|
||
Icon: Equal,
|
||
};
|
||
}
|
||
}
|
||
|
||
function tipIcon(sev: CoachSeverity) {
|
||
switch (sev) {
|
||
case "ok":
|
||
return <CheckCircle2 size={12} className="mt-0.5 shrink-0 text-emerald-600" />;
|
||
case "block":
|
||
return <AlertTriangle size={12} className="mt-0.5 shrink-0 text-red-600" />;
|
||
case "warn":
|
||
return <AlertTriangle size={12} className="mt-0.5 shrink-0 text-amber-600" />;
|
||
default:
|
||
return <Lightbulb size={12} className="mt-0.5 shrink-0 text-[var(--accent)]" />;
|
||
}
|
||
}
|
||
|
||
export default function DofStatusBar() {
|
||
const nodes = useDiagramStore((s) => s.nodes) as Node<EntropykNodeData>[];
|
||
const edges = useDiagramStore((s) => s.edges);
|
||
const [open, setOpen] = useState(false);
|
||
const [tab, setTab] = useState<"guide" | "ledger">("guide");
|
||
|
||
const coach = useMemo(() => buildDofCoach(nodes, edges), [nodes, edges]);
|
||
const { ledger } = coach;
|
||
const style = balanceStyle(ledger.balance);
|
||
const Icon = style.Icon;
|
||
|
||
return (
|
||
<div
|
||
className={`border-t ${style.border} ${style.bg} text-[11px] ${style.fg}`}
|
||
role="status"
|
||
aria-live="polite"
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpen((v) => !v)}
|
||
className="flex w-full items-center gap-3 px-3 py-1.5 text-left transition-colors hover:brightness-[0.98]"
|
||
title="Guide de balance DoF — cliquer pour les étapes"
|
||
>
|
||
<Icon size={14} className="shrink-0" />
|
||
<span className="font-semibold tracking-tight">
|
||
{ledger.balance === "balanced"
|
||
? "Balance OK"
|
||
: ledger.balance === "over-constrained"
|
||
? "Trop de Fixed"
|
||
: ledger.balance === "under-constrained"
|
||
? "Manque de Fixed"
|
||
: "Balance"}
|
||
</span>
|
||
|
||
<span className="mono flex items-center gap-1.5 rounded border border-black/5 bg-white/70 px-2 py-0.5 font-medium">
|
||
<span title="Équations">{ledger.nEquations}</span>
|
||
<span className="text-[var(--ink-faint)]">
|
||
{ledger.balance === "balanced"
|
||
? "="
|
||
: ledger.balance === "over-constrained"
|
||
? ">"
|
||
: "<"}
|
||
</span>
|
||
<span title="Inconnues">{ledger.nUnknowns}</span>
|
||
</span>
|
||
|
||
<span className="hidden min-w-0 flex-1 truncate sm:inline text-[10px] opacity-90">
|
||
{coach.headline}
|
||
</span>
|
||
|
||
<span className="mono ml-auto hidden items-center gap-2 text-[10px] text-[var(--ink-faint)] md:flex">
|
||
<ApiHealthIndicator />
|
||
<Sparkles size={11} className="text-[var(--accent)]" />
|
||
<span>{coach.tips.length} tip{coach.tips.length > 1 ? "s" : ""}</span>
|
||
<span>·</span>
|
||
<span>{ledger.nEdges} edges</span>
|
||
<span>·</span>
|
||
<span>{ledger.nBranches} ṁ</span>
|
||
</span>
|
||
|
||
<ChevronUp
|
||
size={14}
|
||
className={`ml-1 shrink-0 transition-transform ${open ? "" : "rotate-180"}`}
|
||
/>
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="max-h-64 overflow-y-auto border-t border-black/5 bg-white/85 px-3 py-2 text-[10px] leading-relaxed text-[var(--ink-dim)]">
|
||
<div className="mb-2 flex items-center gap-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => setTab("guide")}
|
||
className={`rounded px-2 py-0.5 text-[10px] font-medium ${
|
||
tab === "guide"
|
||
? "bg-[var(--ink)] text-white"
|
||
: "bg-[var(--chrome-2)] text-[var(--ink-dim)]"
|
||
}`}
|
||
>
|
||
Guide
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setTab("ledger")}
|
||
className={`rounded px-2 py-0.5 text-[10px] font-medium ${
|
||
tab === "ledger"
|
||
? "bg-[var(--ink)] text-white"
|
||
: "bg-[var(--chrome-2)] text-[var(--ink-dim)]"
|
||
}`}
|
||
>
|
||
Ledger
|
||
</button>
|
||
<span className="ml-auto text-[9px] text-[var(--ink-faint)]">
|
||
Estimation UI — le CLI valide le ledger Rust exact
|
||
</span>
|
||
</div>
|
||
|
||
{tab === "guide" ? (
|
||
<>
|
||
<p className="mb-2 text-[10px] text-[var(--ink-faint)]">
|
||
Chaque <strong className="text-[var(--ink)]">Fixed</strong> doit libérer une
|
||
inconnue ailleurs (Free P, actionneur, pression émergente…). Suit les étapes dans
|
||
l’ordre.
|
||
</p>
|
||
<ol className="space-y-1.5">
|
||
{coach.tips.map((tip, i) => (
|
||
<li
|
||
key={tip.id}
|
||
className="flex gap-2 rounded border border-[var(--line)] bg-white px-2 py-1.5"
|
||
>
|
||
<span className="mono mt-0.5 w-4 shrink-0 text-[9px] font-bold text-[var(--ink-faint)]">
|
||
{String(i + 1).padStart(2, "0")}
|
||
</span>
|
||
{tipIcon(tip.severity)}
|
||
<div className="min-w-0">
|
||
<div className="font-semibold text-[var(--ink)]">{tip.title}</div>
|
||
<div className="text-[var(--ink-dim)]">{tip.action}</div>
|
||
{tip.focus && (
|
||
<div className="mono mt-0.5 text-[9px] text-[var(--accent)]">
|
||
→ {tip.focus}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</>
|
||
) : (
|
||
<>
|
||
{ledger.diagnostics.length > 0 && (
|
||
<ul className="mb-2 space-y-0.5">
|
||
{ledger.diagnostics.slice(0, 12).map((d, i) => (
|
||
<li key={i} className="flex gap-1.5">
|
||
<span className="text-amber-600">!</span>
|
||
<span>{d}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2 lg:grid-cols-3">
|
||
{ledger.components.map((c) => (
|
||
<div
|
||
key={`${c.name}-${c.type}`}
|
||
className="rounded border border-[var(--line)] bg-white px-2 py-1"
|
||
>
|
||
<div className="flex items-baseline justify-between gap-2">
|
||
<span className="mono font-semibold text-[var(--ink)]">{c.name}</span>
|
||
<span className="mono text-[var(--ink-faint)]">{c.nEquations} eq</span>
|
||
</div>
|
||
<div className="mono truncate text-[9px] text-[var(--ink-faint)]">{c.type}</div>
|
||
<div className="mt-0.5 truncate text-[9px] text-[var(--ink-dim)]">
|
||
{c.roles.join(" · ")}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|