Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.

Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -0,0 +1,230 @@
"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 {
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">
<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>
);
}