fpl-solver / frontend /src /components /SolverOutputPanel.jsx
AnayShukla's picture
updates
4bf65e0
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>
);
};