fpl-solver / frontend /src /components /AdvancedSettingsModal.jsx
AnayShukla's picture
updates
052f3f2
import React, { useState } from "react";
import { X, Power, Info, RotateCcw } from "lucide-react";
// The absolute baseline FPL defaults as defined in your comprehensive_settings.json
export const DEFAULT_SETTINGS = {
enabled: false,
secs: 600,
hit_limit: 0,
no_transfer_last_gws: 0,
keep_top_ev_percent: 7,
ft_use_penalty: 0.1,
itb_loss_per_transfer: 0.0,
vcap_weight: 0.1,
opposing_play_penalty: 0.5,
use_ft_value_list: false, // Sub-toggle for FT behavior
ft_value: 1.5,
xmin_lb: 45,
ft_value_list: { "2": 2, "3": 1.6, "4": 1.3, "5": 1.1 },
bench_weights: { "0": 0.03, "1": 0.21, "2": 0.06, "3": 0.002 },
randomization_strength: 1.0,
iteration_criteria: "this_gw_transfer_in_out",
iteration_diff: 2,
};
export function AdvancedSettingsModal({
setShowAdvancedSettings,
comprehensiveSettings,
setComprehensiveSettings,
}) {
const [isEnabled, setIsEnabled] = useState(
comprehensiveSettings.enabled ?? false
);
const handleToggle = () => {
const newState = !isEnabled;
setIsEnabled(newState);
setComprehensiveSettings((prev) => ({ ...prev, enabled: newState }));
};
const handleResetToDefaults = () => {
if (window.confirm("Are you sure you want to restore all advanced settings to their recommended defaults?")) {
// Restore all defaults, but preserve whatever the current toggle state is!
setComprehensiveSettings({ ...DEFAULT_SETTINGS, enabled: isEnabled });
}
};
const handleChange = (key, value, nestedKey = null) => {
setComprehensiveSettings((prev) => {
if (nestedKey !== null) {
return {
...prev,
[key]: {
...(prev[key] || {}),
[nestedKey]: value,
},
};
}
return { ...prev, [key]: value };
});
};
// Check if we are using the dynamic list or the flat value
const isUsingFtList = comprehensiveSettings.use_ft_value_list ?? DEFAULT_SETTINGS.use_ft_value_list;
const SETTINGS_GROUPS = [
{
title: "Solver Constraints",
items: [
{ key: "secs", label: "Solve Time Limit (secs)", type: "number", step: "1", default: DEFAULT_SETTINGS.secs, desc: "Maximum time in seconds allowed for the solver per iteration." },
{ key: "hit_limit", label: "Max Horizon Hits", type: "number", step: "1", default: DEFAULT_SETTINGS.hit_limit, desc: "Maximum total hits allowed over the entire horizon. Leave blank for infinite." },
{ key: "no_transfer_last_gws", label: "No Transfers Last X GWs", type: "number", step: "1", default: DEFAULT_SETTINGS.no_transfer_last_gws, desc: "Prevent transfers in the final X gameweeks of the horizon." },
{ key: "xmin_lb", label: "Min xMins (Per GW)", type: "number", step: "1", default: DEFAULT_SETTINGS.xmin_lb, desc: "Minimum expected minutes per GW. Multiplied by the horizon length to filter out non-playing players before solving." },
{ key: "keep_top_ev_percent", label: "Keep Top EV (%)", type: "number", step: "1", default: DEFAULT_SETTINGS.keep_top_ev_percent, desc: "Percentage of top EV players to keep for the solve." },
]
},
{
title: "Penalties & Weights",
items: [
{ key: "ft_use_penalty", label: "FT Use Penalty", type: "number", step: "0.01", default: DEFAULT_SETTINGS.ft_use_penalty, desc: "Penalty applied for using a free transfer (encourages rolling)." },
{ key: "itb_loss_per_transfer", label: "ITB Loss per Transfer", type: "number", step: "0.01", default: DEFAULT_SETTINGS.itb_loss_per_transfer, desc: "Artificial cost deducted from ITB per transfer to prefer cheaper identical-EV moves." },
{ key: "vcap_weight", label: "Vice-Captain Weight", type: "number", step: "0.01", default: DEFAULT_SETTINGS.vcap_weight, desc: "Fractional EV added to the Vice Captain in case the main captain does not play." },
{ key: "opposing_play_penalty", label: "Opposing Play Penalty", type: "number", step: "0.01", default: DEFAULT_SETTINGS.opposing_play_penalty, desc: "Penalty applied when attacking players face defensive players in your lineup." },
]
},
{
title: "Bench Weights",
desc: "Fractional EV added to bench players based on their bench order.",
isNested: true,
parentKey: "bench_weights",
items: [
{ nestedKey: "0", label: "Goalkeeper", type: "number", step: "0.01", default: DEFAULT_SETTINGS.bench_weights["0"] },
{ nestedKey: "1", label: "Outfield 1st", type: "number", step: "0.01", default: DEFAULT_SETTINGS.bench_weights["1"] },
{ nestedKey: "2", label: "Outfield 2nd", type: "number", step: "0.01", default: DEFAULT_SETTINGS.bench_weights["2"] },
{ nestedKey: "3", label: "Outfield 3rd", type: "number", step: "0.01", default: DEFAULT_SETTINGS.bench_weights["3"] },
]
},
{
title: "Iterations & Simulations",
items: [
{ key: "randomization_strength", label: "Randomization Strength", type: "number", step: "0.01", default: DEFAULT_SETTINGS.randomization_strength, desc: "Multiplier/Strength for adding noise to EVs during Sensitivity Analysis." },
{ key: "iteration_criteria", label: "Iteration Criteria", type: "select", default: DEFAULT_SETTINGS.iteration_criteria, desc: "Rule to generate alternative solutions in subsequent iterations.",
options: [
{ value: "this_gw_transfer_in_out", label: "Transfers In & Out (Current GW)" },
{ value: "this_gw_transfer_in", label: "Transfers In (Current GW)" },
{ value: "this_gw_transfer_out", label: "Transfers Out (Current GW)" },
]
},
{ key: "iteration_diff", label: "Iteration Difference", type: "number", step: "1", default: DEFAULT_SETTINGS.iteration_diff, desc: "Minimum number of transfers that must change to find an alternate optimal solution." }
]
}
];
return (
<div className="fixed inset-0 z-modal flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-slate-950 border border-slate-800 w-full max-w-3xl max-h-[90vh] overflow-y-auto rounded-2xl flex flex-col shadow-2xl">
{/* Header */}
<div className="sticky top-0 z-sticky bg-slate-950/90 backdrop-blur-md border-b border-slate-800 px-6 py-4 flex items-center justify-between">
<div>
<h3 className="text-xl font-black text-slate-100 flex items-center gap-2">
Advanced Algorithm Settings
</h3>
<p className="text-xs text-slate-400 mt-1">Configure comprehensive internal MILP parameters and weights.</p>
</div>
<div className="flex items-center gap-3">
<button onClick={handleResetToDefaults} className="flex items-center gap-1.5 text-xs font-bold text-slate-400 hover:text-red-400 transition-colors bg-slate-900 px-3 py-2 rounded-lg border border-slate-800 hover:border-red-500/30">
<RotateCcw size={14} /> Defaults
</button>
<button onClick={() => setShowAdvancedSettings(false)} className="text-slate-500 hover:text-white transition-colors bg-slate-900 p-2 rounded-lg border border-slate-800">
<X size={20} />
</button>
</div>
</div>
{/* Body */}
<div className="p-6 flex flex-col gap-8">
{/* Master Toggle */}
<div className={`p-4 rounded-xl border flex items-center justify-between transition-colors shadow-lg ${isEnabled ? 'bg-luigi-500/10 border-luigi-500/30' : 'bg-slate-900 border-slate-700'}`}>
<div className="flex items-center gap-4">
<div className={`p-2.5 rounded-lg ${isEnabled ? 'bg-luigi-500/20 text-luigi-400' : 'bg-slate-800 text-slate-500'}`}>
<Power size={22} />
</div>
<div>
<h4 className={`font-bold text-lg ${isEnabled ? 'text-luigi-400' : 'text-slate-400'}`}>Enable Advanced Overrides</h4>
<p className="text-xs text-slate-500">When enabled, these parameters will be injected into the solver payload.</p>
</div>
</div>
<button
onClick={handleToggle}
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors focus:outline-none ${isEnabled ? 'bg-luigi-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${isEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
<div className={`flex flex-col gap-8 transition-opacity duration-300 ${!isEnabled ? 'opacity-30 pointer-events-none grayscale' : 'opacity-100'}`}>
{/* SPECIAL SECTION: Free Transfer Valuation */}
<div className="bg-slate-900/40 rounded-2xl p-5 border border-slate-800/60">
<div className="mb-5 border-b border-slate-800 pb-3 flex items-center justify-between">
<div>
<h4 className="text-sm font-black text-slate-400 uppercase tracking-widest">FT Val</h4>
<p className="text-[11px] text-slate-500 mt-1">Intrinsic EV value assigned for holding/rolling free transfers.</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-slate-400">Dynamic List</span>
<button onClick={() => handleChange("use_ft_value_list", !isUsingFtList)} className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${isUsingFtList ? 'bg-luigi-500' : 'bg-slate-700'}`}>
<span className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${isUsingFtList ? 'translate-x-5' : 'translate-x-1'}`} />
</button>
</div>
</div>
{isUsingFtList ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{["2", "3", "4", "5"].map((num) => (
<div key={`ft_${num}`} className="bg-slate-900 border border-slate-700 p-4 rounded-xl">
<label className="text-xs font-bold text-slate-300 block mb-2">Value of {num}{num==='2'?'nd':num==='3'?'rd':'th'} FT</label>
<input type="number" step="0.1" value={comprehensiveSettings.ft_value_list?.[num] ?? DEFAULT_SETTINGS.ft_value_list[num]} onChange={(e) => handleChange("ft_value_list", parseFloat(e.target.value) || 0, num)} className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-luigi-500 font-mono" />
</div>
))}
</div>
) : (
<div className="bg-slate-900/50 border border-slate-700 p-4 rounded-xl flex items-center justify-center text-center h-[88px]">
<p className="text-xs text-slate-400 font-bold">Using standard flat FT Value from normal settings.</p>
</div>
)}
</div>
{/* Render Remaining Standard Settings Groups */}
{SETTINGS_GROUPS.map((group, idx) => (
<div key={idx} className="bg-slate-900/40 rounded-2xl p-5 border border-slate-800/60">
<div className="mb-5 border-b border-slate-800 pb-3">
<h4 className="text-sm font-black text-slate-400 uppercase tracking-widest">{group.title}</h4>
{group.desc && <p className="text-[11px] text-slate-500 mt-1">{group.desc}</p>}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{group.items.map((item) => {
let val = group.isNested
? (comprehensiveSettings[group.parentKey]?.[item.nestedKey] ?? item.default)
: (comprehensiveSettings[item.key] ?? item.default);
return (
<div key={item.key || item.nestedKey} className="bg-slate-900 border border-slate-700 p-4 rounded-xl relative group hover:border-slate-500 transition-colors">
<div className="flex justify-between items-center mb-2">
<label className="text-xs font-bold text-slate-300">{item.label}</label>
<Info size={14} className="text-slate-600 group-hover:text-luigi-400 transition-colors" />
</div>
{item.type === "select" ? (
<select value={val} onChange={(e) => handleChange(item.key, e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-luigi-500 cursor-pointer">
{item.options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
) : (
<input
type={item.type}
step={item.step}
min={item.min}
// THE FIX 1: If it's a checkbox, use 'checked'. Otherwise use 'value'.
checked={item.type === "checkbox" ? Boolean(val) : undefined}
value={item.type !== "checkbox" ? (val === "" ? "" : val) : undefined}
placeholder={item.default === "" ? "None" : item.default}
onChange={(e) => {
// THE FIX 2: Safely extract boolean for checkboxes, numbers for everything else
let newVal;
if (item.type === "checkbox") {
newVal = e.target.checked;
} else {
newVal = e.target.value === "" ? "" : (parseFloat(e.target.value) || 0);
}
group.isNested ? handleChange(group.parentKey, newVal, item.nestedKey) : handleChange(item.key, newVal);
}}
className={`bg-slate-950 border border-slate-700 rounded-lg text-sm text-slate-100 focus:outline-none focus:border-luigi-500 font-mono ${item.type === 'checkbox' ? 'w-5 h-5 accent-luigi-500 cursor-pointer' : 'w-full px-3 py-2'}`}
/>
)}
<div className="absolute left-0 -bottom-2 translate-y-full opacity-0 group-hover:opacity-100 transition-opacity z-tooltip w-[110%] bg-slate-800 text-slate-300 text-[11px] p-2.5 rounded-lg shadow-xl pointer-events-none border border-slate-700">
{item.desc || group.desc}
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}