Spaces:
Running
Running
| import React from "react"; | |
| import { Zap, ExternalLink } from "lucide-react"; | |
| import { CHIP_CONFIG } from "../utils/fplLogic"; | |
| export const SolverOutputPanel = ({ | |
| pendingSolutions, setPendingSolutions, isSolving, globalPlayers, applySolution, appliedPlanSummary, setAppliedPlanSummary, baselineEv = 0,comprehensiveSettings | |
| }) => { | |
| // BULLETPROOF RELATIVE EV | |
| const getRelativeEv = (sol) => { | |
| if (baselineEv === undefined || !sol) return "+0.00"; | |
| const base = sol.lockedBaselineEv !== undefined ? sol.lockedBaselineEv : baselineEv; | |
| if (typeof sol === "number") { | |
| const diff = sol - base; | |
| return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); | |
| } | |
| 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; | |
| 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 { | |
| // THE FIX: Output Panel now perfectly matches Python and the main UI! | |
| const rawBw = comprehensiveSettings?.bench_weights || { 0: 0.03, 1: 0.21, 2: 0.06, 3: 0.002 }; | |
| const gkWeight = Number(rawBw[0] || 0.03); | |
| const outWeights = [Number(rawBw[1] || 0.21), Number(rawBw[2] || 0.06), Number(rawBw[3] || 0.002)]; | |
| if (p.Pos === "G") { | |
| gwPts += pts * gkWeight; | |
| } else { | |
| gwPts += pts * (outWeights[ofIdx] || 0.02); | |
| ofIdx++; | |
| } | |
| } | |
| } | |
| }); | |
| pathEV += gwPts - (gwPlan.hits || 0) * 4; | |
| }); | |
| 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 ( | |
| <div className="w-full bg-slate-950 border border-slate-800 rounded-2xl flex flex-col h-auto shadow-2xl overflow-hidden relative min-h-[320px]"> | |
| <div className="border-b border-slate-800 px-5 py-4 bg-slate-900/50 flex items-center justify-between"> | |
| {/* Left Side: Original Title & Description */} | |
| <div className="flex flex-col"> | |
| <h2 className="text-sm font-black uppercase tracking-widest text-slate-300">Solver output</h2> | |
| <p className="text-[10px] text-slate-500 mt-1">Nothing changes your squad until you apply a path.</p> | |
| </div> | |
| {/* Right Side: Sleek, Minimalist Credit */} | |
| {/* Right Side: Sleek, Minimalist Credit */} | |
| <a | |
| href="https://github.com/sertalpbilal/FPL-Optimization-Tools" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="group flex items-center gap-1 text-[9px] font-bold uppercase tracking-widest transition-colors text-right whitespace-nowrap ml-4 shrink-0" | |
| > | |
| <span className="text-slate-600">Credit</span> | |
| <span className="text-slate-500 group-hover:text-luigi-400 transition-colors">Sertalp-Moose Solver</span> | |
| <ExternalLink size={10} className="text-slate-600 group-hover:text-luigi-400 transition-colors" /> | |
| </a> | |
| </div> | |
| <div className="flex-1 flex flex-col p-5 overflow-y-auto custom-scrollbar"> | |
| {pendingSolutions.length > 0 && !isSolving && ( | |
| <div className="flex flex-col gap-4"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <h3 className="text-slate-200 font-black">Optimal Paths Found</h3> | |
| <button onClick={() => setPendingSolutions([])} className="text-xs text-slate-500 hover:text-red-400 font-bold uppercase transition-colors">Clear</button> | |
| </div> | |
| {pendingSolutions.map((sol, index) => ( | |
| <div key={index} className="bg-slate-900 border border-luigi-500/30 rounded-xl p-4 flex flex-col gap-4"> | |
| <div className="flex justify-between items-center border-b border-slate-800 pb-2"> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-black text-slate-300">ITERATION {sol.id || index + 1}</span> | |
| {sol.chips_used && Object.entries(sol.chips_used).map(([gw, chip]) => { | |
| const cfg = CHIP_CONFIG[chip]; | |
| return cfg ? <span key={gw} className={`text-[9px] font-black px-1.5 py-0.5 rounded ${cfg.badge}`} title={`${cfg.label} in GW${gw}`}>{cfg.short}{gw}</span> : null; | |
| })} | |
| </div> | |
| <div className="flex flex-col items-end gap-0.5"> | |
| <span className="text-luigi-400 font-mono font-bold text-sm">{getRelativeEv(sol)} pts</span> | |
| {sol.objective_score != null && <span className="text-slate-400 font-mono text-[10px]">eval: {sol.objective_score.toFixed(2)}</span>} | |
| </div> | |
| </div> | |
| <div className="flex flex-col gap-2"> | |
| {sol.plan.map((gwPlan) => ( | |
| (gwPlan.transfers_in.length > 0 || gwPlan.transfers_out.length > 0) && ( | |
| <div key={gwPlan.gw} className="bg-slate-950 rounded p-2 text-xs"> | |
| <div className="text-slate-500 font-bold mb-2 flex justify-between items-center"> | |
| <div className="flex items-center gap-2"> | |
| <span className="bg-slate-800 px-2 py-1 rounded text-slate-300">GW {gwPlan.gw}</span> | |
| {gwPlan.chip && CHIP_CONFIG[gwPlan.chip] && <span className={`text-[9px] font-black px-1.5 py-0.5 rounded ${CHIP_CONFIG[gwPlan.chip].badge}`}>{CHIP_CONFIG[gwPlan.chip].short}{gwPlan.gw}</span>} | |
| </div> | |
| <div className="flex gap-3"> | |
| <span className="text-emerald-400 font-mono">ITB: £{Math.abs(gwPlan.itb) < 0.05 ? "0.0" : Number(gwPlan.itb).toFixed(1)}</span> | |
| <span className="text-cyan-400 font-mono">FT Spend: {gwPlan.chip === "wc" || gwPlan.chip === "fh" ? `${gwPlan.transfers_out?.length || 0}/∞` : `${gwPlan.transfers_out?.length || 0}/${gwPlan.ft_at_start ?? 1}${gwPlan.hits > 0 ? ` (-${gwPlan.hits * 4})` : ""}`}</span> | |
| </div> | |
| </div> | |
| {gwPlan.chip === "wc" || gwPlan.chip === "fh" ? <p className="text-[10px] text-slate-500 italic mb-1">{gwPlan.chip === "wc" ? "Wildcard active — unlimited free transfers" : "Free Hit active — squad reverts after the FH"}</p> : null} | |
| {gwPlan.transfers_out.map((id, i) => ( | |
| <div key={i} className="flex justify-between items-center text-slate-300 font-mono py-0.5"> | |
| <span className="text-red-400 truncate w-[40%]">{globalPlayers.find((p) => String(p.ID) === String(id))?.Name || id}</span> | |
| <span className="text-slate-600 font-bold">»</span> | |
| <span className="text-emerald-400 truncate w-[40%] text-right">{globalPlayers.find((p) => String(p.ID) === String(gwPlan.transfers_in[i]))?.Name || gwPlan.transfers_in[i]}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ) | |
| ))} | |
| </div> | |
| <button onClick={() => applySolution(sol)} className="w-full bg-slate-800 hover:bg-luigi-500 hover:text-slate-950 text-luigi-400 font-bold py-2 rounded-lg transition-colors text-sm">Apply Path</button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {!isSolving && pendingSolutions.length === 0 && ( | |
| appliedPlanSummary ? ( | |
| <div className="flex flex-col gap-3 p-1"> | |
| <div className="flex items-center justify-between mb-1"> | |
| <h4 className="text-slate-300 font-bold text-xs uppercase tracking-wider">Last Applied · {appliedPlanSummary.horizon}</h4> | |
| <button onClick={() => setAppliedPlanSummary(null)} className="text-slate-600 hover:text-red-400 text-xs font-bold">✕</button> | |
| </div> | |
| <div className="text-[10px] text-slate-500 font-mono">{getRelativeEv(appliedPlanSummary)} pts {appliedPlanSummary.objectiveScore != null && ` · eval ${appliedPlanSummary.objectiveScore.toFixed(2)}`}</div> | |
| {appliedPlanSummary.transfers.map((t, i) => ( | |
| <div key={i} className="bg-slate-900 rounded-lg p-2.5 text-xs"> | |
| <div className="flex items-center justify-between gap-2 mb-1.5"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-slate-400 font-bold">GW {t.gw}</span> | |
| {t.chip && CHIP_CONFIG[t.chip] && <span className={`text-[9px] px-1 py-0.5 rounded font-black ${CHIP_CONFIG[t.chip].badge}`}>{CHIP_CONFIG[t.chip].short}{t.gw}</span>} | |
| </div> | |
| <div className="flex gap-2 text-[9px] font-mono"> | |
| <span className="text-cyan-400">FT Spend: {t.chip === "wc" || t.chip === "fh" ? `${t.outs?.length || 0}/∞` : `${t.outs?.length || 0}/${t.ft_at_start ?? 1}${t.hits > 0 ? ` (-${t.hits * 4})` : ""}`}</span> | |
| <span className="text-emerald-400">£{Math.abs(t.itb) < 0.05 ? "0.0" : Number(t.itb).toFixed(1)}m</span> | |
| </div> | |
| </div> | |
| {t.outs.length === 0 && t.ins.length === 0 ? ( | |
| <span className="text-slate-600 italic text-[10px]">{t.chip ? `${CHIP_CONFIG[t.chip]?.label || t.chip} active` : 'Hold — no transfers'}</span> | |
| ) : ( | |
| t.outs.map((name, j) => ( | |
| <div key={j} className="flex items-center gap-1 py-0.5 font-mono"> | |
| <span className="text-red-400 truncate flex-1">{name}</span> | |
| <span className="text-slate-600 font-bold shrink-0">»</span> | |
| <span className="text-emerald-400 truncate flex-1 text-right">{t.ins[j] || "?"}</span> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col items-center justify-center gap-3 min-h-[200px] text-slate-500 text-sm text-center px-4"> | |
| <Zap size={28} className="text-slate-700" /> | |
| Configure settings and hit <span className="text-luigi-400 font-bold">Solve</span> in the left panel. | |
| </div> | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; |