Spaces:
Running
Running
| import React, { useContext, useState } from "react"; | |
| import { Settings } from "lucide-react"; | |
| import { CHIP_CONFIG } from "../utils/fplLogic"; | |
| import { PlayerContext } from "../PlayerContext"; | |
| import { FixtureMatrixPanel } from "./FixtureMatrixPanel"; | |
| export const TabsPanel = ({ | |
| solverTab, setSolverTab, | |
| isSolving, isRunningSens, isChipSolving, | |
| runMainSolver, runSensAnalysis, runChipSolve, | |
| setShowAdvancedSettings, | |
| quickSettings, setQuickSettings, | |
| banSearch, setBanSearch, lockSearch, setLockSearch, | |
| globalPlayers, teamData, solveGWLabel, | |
| numSims, setNumSims, sensResults, setSensResults, sensViewGw, setSensViewGw, | |
| chipSolveOptions, setChipSolveOptions, chipSolveSolutions, setChipSolveSolutions, horizonGWs, baselineEv = 0 | |
| }) => { | |
| const [lockGwSearch, setLockGwSearch] = useState(""); | |
| const [banGwSearch, setBanGwSearch] = useState(""); | |
| const { fixtureOverrides, setFixtureOverrides, availableGWs } = useContext(PlayerContext); | |
| const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : ""; | |
| const getRelativeEv = (sol) => { | |
| if (baselineEv === undefined || !sol) return "+0.00"; | |
| // Prevent 0.0 bug: Use locked baseline if it's the "Last Applied" summary | |
| const base = sol.lockedBaselineEv !== undefined ? sol.lockedBaselineEv : baselineEv; | |
| // Prevent NaN bug: Safely handle if sol is just a raw number by accident | |
| if (typeof sol === "number") { | |
| const diff = sol - base; | |
| return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); | |
| } | |
| // Prevent NaN bug: If the API didn't return a full detailed plan (like in Chip Solve) | |
| if (!sol.plan || !Array.isArray(sol.plan) || sol.plan.length === 0) { | |
| const fallbackEv = sol.ev !== undefined ? sol.ev : 0; | |
| const diff = fallbackEv - base; | |
| return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); | |
| } | |
| let pathEV = 0; | |
| let hasValidGw = false; | |
| sol.plan.forEach(gwPlan => { | |
| const gw = gwPlan.gw; | |
| if (gw === undefined) return; // Prevents checking p['undefined_Pts'] | |
| hasValidGw = true; | |
| const gwChip = gwPlan.chip; | |
| const gwCapMult = gwChip === "tc" ? 3 : 2; | |
| let gwPts = 0; | |
| const getPlayer = (id) => globalPlayers.find(p => String(p.ID) === String(id)); | |
| (gwPlan.lineup || []).forEach(id => { | |
| const p = getPlayer(id); | |
| if (p && !p.isBlank) { | |
| const pts = Number(p[`${gw}_Pts`]) || 0; | |
| gwPts += pts * (String(p.ID) === String(gwPlan.captain) ? gwCapMult : 1); | |
| } | |
| }); | |
| let ofIdx = 0; | |
| (gwPlan.bench || []).forEach(id => { | |
| const p = getPlayer(id); | |
| if (p && !p.isBlank) { | |
| const pts = Number(p[`${gw}_Pts`]) || 0; | |
| if (gwChip === "bb") { | |
| gwPts += pts; | |
| } else if (p.Pos === "G") { | |
| gwPts += pts * 0.04; | |
| } else { | |
| gwPts += pts * ([0.17, 0.05, 0.02][ofIdx] || 0.02); | |
| ofIdx++; | |
| } | |
| } | |
| }); | |
| pathEV += gwPts - (gwPlan.hits || 0) * 4; | |
| }); | |
| // Failsafe in case the plan existed but couldn't be parsed properly | |
| if (!hasValidGw || Number.isNaN(pathEV)) { | |
| const fallbackEv = sol.ev !== undefined ? sol.ev : 0; | |
| const diff = fallbackEv - base; | |
| return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); | |
| } | |
| const diff = pathEV - base; | |
| return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); | |
| }; | |
| return ( | |
| <> | |
| <button type="button" onClick={() => setShowAdvancedSettings(true)} className="absolute top-3 right-3 text-slate-500 hover:text-luigi-400 transition-colors z-dropdown" aria-label="Comprehensive settings"><Settings size={20} /></button> | |
| <div className="flex border-b border-slate-800/50 bg-slate-900/50 pr-12"> | |
| {["solver", "sensitivity", "chips", "fixtures"].map((tab) => ( | |
| <button key={tab} type="button" onClick={() => setSolverTab(tab)} className={`flex-1 py-4 text-xs font-black uppercase tracking-widest transition-colors ${solverTab === tab ? "text-luigi-400 border-b-2 border-luigi-400" : "text-slate-500 hover:text-slate-300"}`}> | |
| {tab === "solver" ? "Solve" : tab === "sensitivity" ? "Sens Anal" : tab === "chips" ? "Chips" : "Fixtures"} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex flex-col p-5 overflow-y-auto custom-scrollbar min-h-[220px]"> | |
| {solverTab === "solver" && ( | |
| <div className="flex flex-col gap-4 flex-1"> | |
| <h3 className="text-slate-300 font-bold uppercase tracking-wider text-xs mb-1">Quick Settings</h3> | |
| {/* TOOLBAR LAYOUT: Readable sizes, allowed to wrap if screen is small */} | |
| <div className="flex flex-wrap items-start gap-3 w-full"> | |
| {/* Decay */} | |
| <div className="flex flex-col gap-1 w-16 sm:w-20 shrink-0"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Decay</label> | |
| <input type="number" step="0.01" value={quickSettings.decay} onChange={(e) => setQuickSettings({ ...quickSettings, decay: e.target.value })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" /> | |
| </div> | |
| {/* FT Val */} | |
| <div className="flex flex-col gap-1 w-16 sm:w-20 shrink-0"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">FT Val</label> | |
| <input type="number" step="0.1" value={quickSettings.ft_value} onChange={(e) => setQuickSettings({ ...quickSettings, ft_value: Number(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" /> | |
| </div> | |
| {/* Iters */} | |
| <div className="flex flex-col gap-1 w-16 sm:w-20 shrink-0"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Iters</label> | |
| <select value={quickSettings.iterations} onChange={e => setQuickSettings({ ...quickSettings, iterations: Number(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500 cursor-pointer"> | |
| {[1, 2, 3, 4, 5].map(i => <option key={i} value={i}>{i}</option>)} | |
| </select> | |
| </div> | |
| {/* Lock */} | |
| <div className="relative flex-1 min-w-[120px] flex flex-col gap-1"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Lock</label> | |
| <input type="text" placeholder="Search..." value={lockSearch} onChange={(e) => setLockSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" /> | |
| {lockSearch && ( | |
| <div className="absolute top-full left-0 w-full bg-slate-800 border border-slate-700 rounded mt-1 max-h-32 overflow-y-auto z-popover shadow-xl"> | |
| {/* THE FIX: Added (quickSettings.locked || []) */} | |
| {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockSearch)) && !(quickSettings.locked || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => ( | |
| <div key={`lock-${p.ID}`} onClick={() => { setQuickSettings((prev) => ({ ...prev, locked: [...(prev.locked || []), p] })); setLockSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> | |
| {p.Name} <span className="text-slate-500">({p.Team})</span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {/* THE FIX: Added (quickSettings.locked || []) */} | |
| {(quickSettings.locked || []).map((p) => ( | |
| <span key={`l-${p.ID}`} className="bg-emerald-900/40 text-emerald-400 text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1 border border-emerald-800 max-w-full"> | |
| <span className="truncate">{p.Name}</span> <button type="button" onClick={() => setQuickSettings((prev) => ({ ...prev, locked: (prev.locked || []).filter((l) => l.ID !== p.ID) }))} className="hover:text-white shrink-0">✕</button> | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Ban */} | |
| <div className="relative flex-1 min-w-[120px] flex flex-col gap-1"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Ban</label> | |
| <input type="text" placeholder="Search..." value={banSearch} onChange={(e) => setBanSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" /> | |
| {banSearch && ( | |
| <div className="absolute top-full left-0 w-full bg-slate-800 border border-slate-700 rounded mt-1 max-h-32 overflow-y-auto z-popover shadow-xl"> | |
| {/* THE FIX: Added (quickSettings.banned || []) */} | |
| {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banSearch)) && !(quickSettings.banned || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => ( | |
| <div key={`ban-${p.ID}`} onClick={() => { setQuickSettings((prev) => ({ ...prev, banned: [...(prev.banned || []), p] })); setBanSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> | |
| {p.Name} <span className="text-slate-500">({p.Team})</span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {/* THE FIX: Added (quickSettings.banned || []) */} | |
| {(quickSettings.banned || []).map((p) => ( | |
| <span key={`b-${p.ID}`} className="bg-red-900/40 text-red-400 text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1 border border-red-800 max-w-full"> | |
| <span className="truncate">{p.Name}</span> <button type="button" onClick={() => setQuickSettings((prev) => ({ ...prev, banned: (prev.banned || []).filter((b) => b.ID !== p.ID) }))} className="hover:text-white shrink-0">✕</button> | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Lock (This GW) */} | |
| <div className="relative flex-1 min-w-[120px] flex flex-col gap-1"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Lock (This GW)</label> | |
| <input type="text" placeholder="Search..." value={lockGwSearch} onChange={(e) => setLockGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" /> | |
| {lockGwSearch && ( | |
| <div className="absolute top-full left-0 w-full bg-slate-800 border border-slate-700 rounded mt-1 max-h-32 overflow-y-auto z-popover shadow-xl"> | |
| {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockGwSearch)) && !(quickSettings.lock_this_gw || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => ( | |
| <div key={`lock-gw-${p.ID}`} onClick={() => { setQuickSettings((prev) => ({ ...prev, lock_this_gw: [...(prev.lock_this_gw || []), p] })); setLockGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> | |
| {p.Name} <span className="text-slate-500">({p.Team})</span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {(quickSettings.lock_this_gw || []).map((p) => ( | |
| <span key={`lgw-${p.ID}`} className="bg-emerald-900/40 text-emerald-400 text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1 border border-emerald-800 max-w-full"> | |
| <span className="truncate">{p.Name}</span> <button type="button" onClick={() => setQuickSettings((prev) => ({ ...prev, lock_this_gw: (prev.lock_this_gw || []).filter((l) => l.ID !== p.ID) }))} className="hover:text-white shrink-0">✕</button> | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Ban (This GW) */} | |
| <div className="relative flex-1 min-w-[120px] flex flex-col gap-1"> | |
| <label className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Ban (This GW)</label> | |
| <input type="text" placeholder="Search..." value={banGwSearch} onChange={(e) => setBanGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" /> | |
| {banGwSearch && ( | |
| <div className="absolute top-full left-0 w-full bg-slate-800 border border-slate-700 rounded mt-1 max-h-32 overflow-y-auto z-popover shadow-xl"> | |
| {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banGwSearch)) && !(quickSettings.ban_this_gw || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => ( | |
| <div key={`ban-gw-${p.ID}`} onClick={() => { setQuickSettings((prev) => ({ ...prev, ban_this_gw: [...(prev.ban_this_gw || []), p] })); setBanGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> | |
| {p.Name} <span className="text-slate-500">({p.Team})</span> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {(quickSettings.ban_this_gw || []).map((p) => ( | |
| <span key={`bgw-${p.ID}`} className="bg-red-900/40 text-red-400 text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1 border border-red-800 max-w-full"> | |
| <span className="truncate">{p.Name}</span> <button type="button" onClick={() => setQuickSettings((prev) => ({ ...prev, ban_this_gw: (prev.ban_this_gw || []).filter((b) => b.ID !== p.ID) }))} className="hover:text-white shrink-0">✕</button> | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <button type="button" onClick={runMainSolver} disabled={teamData.some((p) => p.isBlank) || teamData.length === 0 || isSolving} className="mt-auto self-center bg-luigi-500 hover:bg-luigi-400 text-slate-950 font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-xs uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"> | |
| Solve {solveGWLabel ? `(${solveGWLabel})` : ""} | |
| </button> | |
| </div> | |
| )} | |
| {solverTab === "sensitivity" && ( | |
| <div className="flex flex-col gap-4 flex-1 overflow-y-auto"> | |
| <p className="text-[10px] text-slate-500 leading-relaxed"> | |
| Runs <span className="text-slate-300 font-bold">N randomised solves</span> with per-player EV noise. | |
| </p> | |
| <div className="flex items-center gap-3"> | |
| <label className="text-xs text-slate-500 font-bold">Simulations</label> | |
| <input type="number" min="2" max="300" value={numSims} onChange={(e) => setNumSims(Math.max(2, Math.min(300, Number(e.target.value))))} className="w-20 bg-slate-900 border border-slate-700 rounded p-2 text-slate-200 font-mono text-sm outline-none focus:border-luigi-500" /> | |
| </div> | |
| <button type="button" onClick={runSensAnalysis} disabled={isRunningSens || teamData.some((p) => p.isBlank) || teamData.length === 0} className="self-center bg-cyan-700 hover:bg-cyan-600 text-white font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-sm uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed"> | |
| {isRunningSens ? `Running...` : `Run Sims ${solveGWLabel ? `(${solveGWLabel})` : ""}`} | |
| </button> | |
| {sensResults && ( | |
| <div className="flex flex-col gap-3 mt-4"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="text-slate-300 font-bold text-xs uppercase tracking-wider">{sensResults.valid_runs}/{sensResults.num_sims} valid</h4> | |
| <button onClick={() => { setSensResults(null); setSensViewGw(null); }} className="text-xs text-slate-500 hover:text-red-400 font-bold uppercase">Clear</button> | |
| </div> | |
| <div className="flex flex-wrap gap-1.5"> | |
| {(sensResults.horizon_gws || []).map((gw) => { | |
| const isChipFree = sensResults.gw_results?.[String(gw)]?.is_chip_free; | |
| const isActive = sensViewGw === gw; | |
| return ( | |
| <button key={gw} type="button" onClick={() => setSensViewGw(gw)} className={`w-8 h-8 rounded-full text-[11px] font-black transition-colors ${isActive ? isChipFree ? "bg-purple-500 text-white" : "bg-cyan-500 text-slate-950" : isChipFree ? "bg-purple-900/60 text-purple-300 hover:bg-purple-800/60 ring-1 ring-purple-500/40" : "bg-slate-800 text-slate-400 hover:bg-slate-700"}`}>{gw}</button> | |
| ); | |
| })} | |
| </div> | |
| {sensViewGw && sensResults.gw_results?.[String(sensViewGw)] && (() => { | |
| const gd = sensResults.gw_results[String(sensViewGw)]; | |
| if (gd.is_chip_free) { | |
| const POS_NAMES = { G: "Goalkeepers", D: "Defenders", M: "Midfielders", F: "Forwards" }; | |
| return ( | |
| <div className="flex flex-col gap-3"> | |
| <div className="text-[10px] text-purple-400 font-bold uppercase tracking-wider">⚡ Wildcard / Free Hit — Squad Selection</div> | |
| {["G", "D", "M", "F"].map((pos) => { | |
| const rows = gd.players?.[pos] || []; | |
| if (!rows.length) return null; | |
| return ( | |
| <div key={pos} className="bg-slate-900 rounded-xl p-3"> | |
| <h5 className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-2">{POS_NAMES[pos]}</h5> | |
| <div className="flex text-[9px] text-slate-500 font-bold uppercase tracking-wider mb-1 gap-2"><span className="flex-1">Player</span><span className="w-12 text-right">Squad</span><span className="w-12 text-right">Lineup</span></div> | |
| {rows.map((r, i) => ( | |
| <div key={i} className="flex items-center py-0.5 text-xs gap-2"> | |
| <div className="flex-1 min-w-0"><div className="h-1 rounded-full bg-purple-600/30 mb-0.5" style={{ width: `${r.squad_pct}%` }} /><span className="text-slate-300 truncate block">{r.name}</span></div> | |
| <span className="text-purple-400 font-mono font-bold w-12 text-right shrink-0">{r.squad_pct}%</span><span className="text-cyan-400 font-mono font-bold w-12 text-right shrink-0">{r.lineup_pct}%</span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="flex flex-col gap-3"> | |
| {gd.no_transfer_pct > 0 && <div className="text-[10px] text-slate-500 italic">Hold (no transfer): {gd.no_transfer_pct}% of sims</div>} | |
| {["moves", "buys", "sells"].map((key) => { | |
| const rows = gd[key] || []; | |
| if (!rows.length) return null; | |
| const titles = { moves: "Moves (Out → In)", buys: "Buys", sells: "Sells" }; | |
| return ( | |
| <div key={key} className="bg-slate-900 rounded-xl p-3"> | |
| <h5 className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-2">{titles[key]}</h5> | |
| {rows.slice(0, 10).map((r, i) => ( | |
| <div key={i} className="flex justify-between items-center py-0.5 text-xs gap-2"> | |
| <div className="flex-1 min-w-0"><div className="h-1 rounded-full bg-cyan-600/30 mb-0.5" style={{ width: `${r.pct}%` }} /><span className="text-slate-300 truncate block">{r.name}</span></div> | |
| <span className="text-cyan-400 font-mono font-bold shrink-0 ml-2">{r.pct}%</span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| })()} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {solverTab === "chips" && ( | |
| <div className="flex flex-col gap-4 flex-1 overflow-y-auto"> | |
| <p className="text-[10px] text-slate-500 leading-relaxed"> | |
| Select which GWs each chip can be played in, then hit <span className="text-purple-400 font-bold">Chip Solve</span>. | |
| </p> | |
| <div className="grid grid-cols-1 gap-3"> | |
| {["wc", "fh", "bb", "tc"].map((key) => { | |
| const cfg = CHIP_CONFIG[key]; | |
| const sel = chipSolveOptions[key] || []; | |
| return ( | |
| <div key={key} className={`bg-slate-900 border ${cfg.border} rounded-xl p-3`}> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className={`text-[11px] font-black ${cfg.text} uppercase tracking-wider`}>{cfg.label}</span> | |
| {sel.length > 0 && <span className={`text-[9px] px-1.5 py-0.5 rounded font-bold ${cfg.badge}`}>{sel.length} GW{sel.length > 1 ? "s" : ""}</span>} | |
| </div> | |
| <div className="flex flex-wrap gap-1 mt-2"> | |
| {horizonGWs.map((gw) => { | |
| const active = sel.includes(gw); | |
| return ( | |
| <button key={gw} type="button" onClick={() => setChipSolveOptions((prev) => ({ ...prev, [key]: active ? prev[key].filter((g) => g !== gw) : [...prev[key], gw] }))} className={`text-[10px] px-1.5 py-0.5 rounded font-bold transition-colors ${active ? cfg.activeBg : cfg.inactiveBg}`}>{gw}</button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <button type="button" onClick={runChipSolve} disabled={isChipSolving || teamData.some((p) => p.isBlank) || teamData.length === 0 || Object.values(chipSolveOptions).every((v) => v.length === 0)} className="self-center mt-2 bg-purple-600 hover:bg-purple-500 text-white font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-sm uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed"> | |
| {isChipSolving ? `Solving...` : "Chip Solve"} | |
| </button> | |
| {chipSolveSolutions.length > 0 && ( | |
| <div className="flex flex-col gap-2 mt-4"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="text-slate-300 font-bold text-xs uppercase tracking-wider">Best Chip Combos</h4> | |
| <button onClick={() => setChipSolveSolutions([])} className="text-xs text-slate-500 hover:text-red-400 font-bold uppercase">Clear</button> | |
| </div> | |
| {chipSolveSolutions.map((sol, i) => { | |
| const combo = sol.chip_combo || {}; | |
| const active = Object.entries(combo) | |
| .filter(([, gws]) => Array.isArray(gws) && gws.length > 0) | |
| .map(([k, gws]) => ({ text: `${k.replace("use_", "").toUpperCase()}${gws[0]}`, gw: gws[0] })) | |
| .sort((a, b) => a.gw - b.gw) | |
| .map(x => x.text); | |
| return ( | |
| <div key={i} className="bg-slate-950 border border-purple-500/20 rounded-lg p-2.5 flex items-center justify-between gap-2"> | |
| <div className="flex items-center gap-0.5"> | |
| {/* 1. Added Rank Number (1., 2., 3...) */} | |
| <span className="text-[11px] font-black text-slate-500 mr-1.5 w-3 text-right">{i + 1}.</span> | |
| <span className="text-[11px] font-bold text-slate-300">{active.length ? active.join(" + ") : "No chips"}</span> | |
| </div> | |
| <div className="flex flex-col items-end shrink-0 leading-tight"> | |
| <span className="text-purple-400 font-mono font-bold text-sm"> | |
| {sol.objective_score != null ? sol.objective_score.toFixed(2) : sol.ev.toFixed(2)} | |
| </span> | |
| {/* 2. EV is now bigger (text-[10px]) and colored purple to match! */} | |
| <span className="text-[10px] text-purple-400/80 font-mono font-bold mt-0.5"> | |
| ({getRelativeEv(sol)} ev) | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {solverTab === "fixtures" && ( | |
| <FixtureMatrixPanel | |
| globalPlayers={globalPlayers} | |
| fixtureOverrides={fixtureOverrides} | |
| setFixtureOverrides={setFixtureOverrides} | |
| availableGWs={availableGWs} | |
| /> | |
| )} | |
| </div> | |
| </> | |
| ); | |
| }; |