/* ============================================================ 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
= 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 && }
); } /* ---------- Coach rail ---------- */ function CoachRail({ open, onClose, tab, setTab, spot, lastFb, session, hand, mode, reviews }) { return ( ); } /* ---------- 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) => ( ))}
{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.
{Object.keys(styles).map((k) => ( ))}
setStack(parseInt(e.target.value))} />
{onDrill && ( <>

Practice a spot

Jump straight into a specific decision and drill it against the solver.
{DRILLS.map((d) => ( ))}
)}
); } window.UI2 = { Header, CoachRail, SettingsModal };