Spaces:
Running
Running
| 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> | |
| ); | |
| } |