Spaces:
Running
Running
File size: 11,578 Bytes
f7cecf3 4bf65e0 f7cecf3 4bf65e0 f7cecf3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | 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>
);
}; |