Files
Entropyk/apps/web/src/components/DofStatusBar.tsx
sepehr 5bd180b5b8
Some checks failed
CI / check (push) Has been cancelled
Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
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>
2026-07-19 16:35:31 +02:00

233 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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
lordre.
</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>
);
}