/* ============================================================
rail.jsx — coaching rail, header, settings modal
============================================================ */
const E2 = window.Engine;
const fmt2 = window.UI.fmt;
const MixBar = window.UI.MixBar;
/* ---------- Header ---------- */
function Header({ mode, setMode, styleKey, session, onSettings, onMenu, muted, onToggleSound }) {
const style = window.GTO.STYLES[styleKey];
const acc = session.decisions > 0 ? Math.round((session.optimal / session.decisions) * 100) : null;
return (
{onMenu && {"☰"} }
GTO HEADS-UP
heads-up no-limit trainer
setMode("play")}>Play
setMode("study")}>Study
VS {style.label}
= 75 ? "var(--good)" : acc >= 55 ? "var(--ok)" : "var(--warn)" }}>
{acc == null ? "\u2014" : acc + "%"}
GTO acc.
= 0 ? "var(--good)" : "var(--bad)" }}>
{session.bb >= 0 ? "+" : ""}{fmt2(session.bb)}
bb won
{onToggleSound && {muted ? "\ud83d\udd07" : "\ud83d\udd0a"} }
{"\u2699"}
);
}
/* ---------- Coach rail ---------- */
function CoachRail({ open, onClose, tab, setTab, spot, lastFb, session, hand, mode, reviews }) {
return (
{"\u2715"}
{["Spot", "Ranges", "Review", "Session", "Log"].map((t) => (
setTab(t)}>{t}
))}
{tab === "Spot" && }
{tab === "Ranges" && }
{tab === "Review" && }
{tab === "Session" && }
{tab === "Log" && }
);
}
/* ---------- Hand-history / leak finder ---------- */
function ReviewRow({ r }) {
const Hole = window.UI.Hole;
const cls = r.color === "good" ? "var(--good)" : r.color === "ok" ? "var(--ok)" : r.color === "warn" ? "var(--warn)" : "var(--bad)";
return (
{r.situation}
{r.correct
? {r.optimalLabel}
: <>{r.chosenLabel} {"\u2192"} {r.optimalLabel} >}
0 ? "var(--bad)" : "var(--text-3)" }}>
{r.evLoss > 0 ? "\u2212" + fmt2(r.evLoss) : "0"}
);
}
function ReviewPanel({ reviews }) {
reviews = reviews || [];
if (!reviews.length) {
return (
Leak Finder
Play some hands. Every graded decision lands here, and your most expensive spots float to the top.
);
}
// aggregate by situation to surface the biggest leaks (ranked by total bb bled)
const groups = {};
for (const r of reviews) {
const g = groups[r.situation] || (groups[r.situation] = { situation: r.situation, n: 0, errs: 0, ev: 0 });
g.n++; if (r.color === "warn" || r.color === "bad") g.errs++; g.ev += r.evLoss;
}
const leaks = Object.values(groups).filter((g) => g.ev > 0.01).sort((a, b) => b.ev - a.ev).slice(0, 3);
const maxEv = leaks.length ? leaks[0].ev : 1;
return (
<>
Top leaks
{leaks.length ? leaks.map((g) => (
{g.situation}
{"\u2212" + fmt2(g.ev)} bb
{g.errs} slip{g.errs === 1 ? "" : "s"} in {g.n} decision{g.n === 1 ? "" : "s"}
)) :
No leaks yet — clean play. Keep going.
}
Recent decisions
{reviews.slice(0, 40).map((r) => )}
>
);
}
function SpotPanel({ spot, lastFb, mode, hand }) {
return (
<>
Live Read
{spot && spot.showEquity ? (
<>
Your equity {fmt2(spot.equity)}%
{spot.potOdds > 0 &&
Pot odds (need) {fmt2(spot.potOdds)}%
}
Pot {fmt2(spot.pot)} bb
SPR {spot.spr == null ? "\u2014" : fmt2(spot.spr)}
>
) : (
{mode === "play"
? "Equity is hidden in Play mode \u2014 trust your reads. Switch to Study to see live numbers."
: "Live equity appears here on your turn."}
)}
Last Decision
{lastFb ?
:
Make a decision to see GTO feedback.
}
>
);
}
function FeedbackBody({ fb }) {
const cls = fb.color === "good" ? "fb-good" : fb.color === "ok" ? "fb-ok" : fb.color === "warn" ? "fb-warn" : "fb-bad";
return (
{window.UI.ratingIcon(fb.color)} {fb.ratingLabel}
{fb.evLoss > 0 ? `\u2212${fmt2(fb.evLoss)} bb` : "0 bb"}
You played {fb.chosenLabel}
GTO play {fb.optimalLabel}
{fb.mixLabel &&
}
{fb.mixLabel &&
{fb.mixSource === "solver" ? "Solver mix" : "GTO mix"} {fb.mixLabel}
}
Equity {fmt2(fb.equity)}%
{fb.potOdds > 0 &&
Pot odds {fmt2(fb.potOdds)}%
}
{fb.note}
);
}
function SessionPanel({ session }) {
const acc = session.decisions > 0 ? (session.optimal / session.decisions) * 100 : 0;
const rows = [
["Hands played", session.hands],
["Decisions", session.decisions],
["GTO-optimal", session.decisions ? `${session.optimal} (${Math.round(acc)}%)` : "\u2014"],
["bb won/lost", (session.bb >= 0 ? "+" : "") + fmt2(session.bb)],
["EV lost to errors", fmt2(session.evLost) + " bb"],
];
const breakdown = [
["Optimal", session.tally.good, "var(--good)"],
["Fine", session.tally.ok, "var(--ok)"],
["Inaccuracy", session.tally.warn, "var(--warn)"],
["Mistake / Blunder", session.tally.bad, "var(--bad)"],
];
return (
<>
This Session
{rows.map((r) => (
{r[0]} {r[1]}
))}
Decision Quality
{breakdown.map((b) => (
{b[0]}
{b[1]}
))}
>
);
}
function LogPanel({ hand }) {
if (!hand) return No hand in progress.
;
const streetName = { preflop: "PREFLOP", flop: "FLOP", turn: "TURN", river: "RIVER", showdown: "SHOWDOWN", done: "" };
const rows = hand.history.map((a, idx) => {
const who = a.player === hand.heroIdx ? "You" : "Bot";
let desc = a.type.toUpperCase();
if (a.type === "call" || a.type === "bet" || a.type === "raise") desc += " " + fmt2(a.to) + "bb";
return (
{who} · {streetName[a.street]}
{desc}
);
});
return (
Action Log
{rows.length ? rows :
New hand — no actions yet.
}
);
}
/* ---------- Range-grid viewer (live CFR solve) ---------- */
function GridCell({ node, code }) {
const S = window.Solver, COLOR = window.UI.ACT_COLOR;
const st = S.strategy(node), f = st && st.freq[code];
if (!f) return {code}
;
const order = ["raise", "bet", "call", "check", "fold"];
const segs = order.map((a) => { const idx = st.names.indexOf(a); return idx >= 0 ? { action: a, freq: f[idx] } : null; })
.filter((x) => x && x.freq > 0.005);
const title = code + " — " + segs.map((s) => s.action + " " + Math.round(s.freq * 100) + "%").join(", ");
return (
{segs.map((s, k) => )}
{code}
);
}
function RangePanel() {
const S = window.Solver;
const ready = S && S.isReady && S.isReady();
const [node, setNode] = React.useState("SB_open");
const [, force] = React.useState(0);
React.useEffect(() => {
if (ready) return;
const t = setInterval(() => force((x) => x + 1), 400);
return () => clearInterval(t);
}, [ready]);
if (!ready) return GTO Ranges Solving the preflop equilibrium… (live CFR — a moment on first load)
;
const RK = window.Engine.RANKS;
const rows = [];
for (let i = 12; i >= 0; i--) {
const cells = [];
for (let j = 12; j >= 0; j--) {
const code = i === j ? RK[i] + RK[i] : i > j ? RK[i] + RK[j] + "s" : RK[j] + RK[i] + "o";
cells.push( );
}
rows.push({cells} );
}
return (
<>
GTO Ranges — live CFR solve
{S.NODES.map((n) => (
setNode(n)}>{S.NODE_LABELS[n]}
))}
{rows}
Raise
Call
Fold
Suited = upper-right, offsuit = lower-left, pairs = diagonal. Hover a cell for exact frequencies. Bars show the solved mix.
Cross-check with a real solver
This grid is the app's own CFR solve (fixed sizings, ~100bb, open-or-fold). For exact frequencies across all sizings and for postflop, study in GTO Wizard or a full solver.
>
);
}
/* ---------- Settings modal ---------- */
const DRILLS = [
["sb_open", "SB open (RFI)", "You're first-in on the button — open or fold."],
["bb_vs_open", "Defend BB vs open", "SB raises to 2.5bb — 3-bet, call, or fold."],
["sb_vs_3bet", "SB vs 3-bet", "You open, BB 3-bets to 9bb — 4-bet, call, or fold."],
["cbet_defense", "Defend vs c-bet", "BB call preflop, then face a ~½-pot flop c-bet."],
];
function SettingsModal({ open, onClose, styleKey, setStyle, stack, setStack, onReset, onDrill }) {
if (!open) return null;
const styles = window.GTO.STYLES;
return (
e.stopPropagation()}>
Table Settings
Pick an opponent to train against. Exploit styles teach you to deviate from GTO.
{"\u2715"}
{Object.keys(styles).map((k) => (
setStyle(k)}>
{k === "gto" && }
{styles[k].label}
{styles[k].desc}
))}
Starting stacks {stack} bb
setStack(parseInt(e.target.value))} />
{onDrill && (
<>
Practice a spot
Jump straight into a specific decision and drill it against the solver.
{DRILLS.map((d) => (
onDrill(d[0])}>
{d[1]}
{d[2]}
))}
>
)}
Reset session stats
Done
);
}
window.UI2 = { Header, CoachRail, SettingsModal };