Spaces:
Running
Running
File size: 15,263 Bytes
f7cecf3 052f3f2 f7cecf3 052f3f2 f7cecf3 052f3f2 f7cecf3 052f3f2 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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | 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>
);
} |