fpl-solver / frontend /src /hooks /useFplSolverApi.js
AnayShukla's picture
updates
1f058dc
import { useState } from "react";
import { getPlayerPrice } from "../utils/fplLogic";
export const useFplSolverApi = (abortControllerRef) => {
const [isSolving, setIsSolving] = useState(false);
const [isChipSolving, setIsChipSolving] = useState(false);
const [isRunningSens, setIsRunningSens] = useState(false);
const [pendingSolutions, setPendingSolutions] = useState([]);
const [chipSolveSolutions, setChipSolveSolutions] = useState([]);
const [sensResults, setSensResults] = useState(null);
const [sensViewGw, setSensViewGw] = useState(null);
// Reusable helper to format players for the backend
const formatMarketPlayers = (globalPlayers, teamData, horizonGWs) => {
return globalPlayers.map((p) => {
const squadPlayer = teamData.find((sp) => sp.ID === p.ID);
const sellPrice = squadPlayer ? squadPlayer.Price : getPlayerPrice(p);
const evs = {};
horizonGWs.forEach((gw) => {
evs[String(gw)] = Number(p[`${gw}_Pts`]) || 0;
});
return {
id: p.ID,
name: p.Name,
pos: p.Pos,
team: p.Team,
now_cost: getPlayerPrice(p),
sell_price: sellPrice,
evs,
};
});
};
// Extracts the basic frontend settings (bans, locks, chips) into a clean dictionary
const buildBaseSettings = (quickSettings, chipsByGw, forceIterations = null) => {
return {
decay_base: Number(quickSettings?.decay || 0.85),
ft_value_base: Number(quickSettings?.ft_value || 1.5),
iterations: forceIterations !== null ? forceIterations : Number(quickSettings?.iterations || 1),
banned_ids: quickSettings?.banned?.map((p) => p.ID) || [],
locked_ids: quickSettings?.locked?.map((p) => p.ID) || [],
ban_this_gw: quickSettings?.ban_this_gw?.map((p) => p.ID) || [],
lock_this_gw: quickSettings?.lock_this_gw?.map((p) => p.ID) || [],
use_wc: Object.entries(chipsByGw || {}).filter(([, c]) => c === "wc").map(([g]) => Number(g)),
use_fh: Object.entries(chipsByGw || {}).filter(([, c]) => c === "fh").map(([g]) => Number(g)),
use_bb: Object.entries(chipsByGw || {}).filter(([, c]) => c === "bb").map(([g]) => Number(g)),
use_tc: Object.entries(chipsByGw || {}).filter(([, c]) => c === "tc").map(([g]) => Number(g)),
};
};
const handleSolve = async ({
teamId, solveGWs, horizonGWs, teamData, globalPlayers, itb, availableFts,
quickSettings, chipsByGw, comprehensiveSettings,
lockedBaselineEv, pastBaselineEv // <-- ADDED HERE
}) => {
setIsSolving(true);
abortControllerRef.current = new AbortController();
try {
const payload = {
team_id: parseInt(teamId, 10) || 0,
horizon_gws: solveGWs,
current_squad_ids: teamData.filter((p) => !p.isBlank && typeof p.ID === "number").map((p) => p.ID),
market_players: formatMarketPlayers(globalPlayers, teamData, horizonGWs),
in_the_bank: itb,
free_transfers: availableFts,
settings: buildBaseSettings(quickSettings, chipsByGw),
comprehensive_settings: comprehensiveSettings,
};
const res = await fetch("https://anayshukla-fpl-solver.hf.space/api/solve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: abortControllerRef.current.signal,
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || data.message || "Solver failed");
if (data.status === "success" && Array.isArray(data.solutions)) {
// THE FIX: Inject the specific baselines and calculate the padded visual total
const enhancedSolutions = data.solutions.map(sol => ({
...sol,
lockedBaselineEv,
paddedTotalEv: (sol.ev || 0) + (pastBaselineEv || 0)
}));
const sorted = [...enhancedSolutions].sort((a, b) => {
const oa = a.objective_score != null ? a.objective_score : a.ev;
const ob = b.objective_score != null ? b.objective_score : b.ev;
if (ob !== oa) return ob - oa;
return (b.ev || 0) - (a.ev || 0);
});
setPendingSolutions(sorted);
} else {
alert("Solver failed: " + (data.message || data.detail || "Unknown"));
}
} catch (err) {
if (err.name === 'AbortError') return;
alert(err.message || "Failed to run the solver.");
} finally {
setIsSolving(false);
}
};
const handleChipSolve = async ({
teamId, horizonGWs, teamData, globalPlayers, itb, availableFts,
quickSettings, comprehensiveSettings, chipSolveOptions,
lockedBaselineEv, pastBaselineEv // <-- ADDED HERE
}) => {
const hasOptions = Object.values(chipSolveOptions).some((v) => v.length > 0);
if (!hasOptions) {
alert("Select at least one GW for a chip before running the chip solve.");
return;
}
setIsChipSolving(true);
setChipSolveSolutions([]);
abortControllerRef.current = new AbortController();
try {
const payload = {
team_id: parseInt(teamId, 10) || 0,
horizon_gws: horizonGWs,
current_squad_ids: teamData.filter((p) => !p.isBlank && typeof p.ID === "number").map((p) => p.ID),
market_players: formatMarketPlayers(globalPlayers, teamData, horizonGWs),
in_the_bank: itb,
free_transfers: availableFts,
settings: buildBaseSettings(quickSettings, {}),
comprehensive_settings: comprehensiveSettings,
chip_gw_options: {
wc: chipSolveOptions.wc,
fh: chipSolveOptions.fh,
bb: chipSolveOptions.bb,
tc: chipSolveOptions.tc,
},
};
const res = await fetch("https://anayshukla-fpl-solver.hf.space/api/chip-solve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: abortControllerRef.current.signal,
});
const data = await res.json();
if (!res.ok) {
const d = data.detail;
const msg = typeof d === "string" ? d : Array.isArray(d) ? d.map((x) => x.msg || x).join(", ") : JSON.stringify(d);
throw new Error(msg || "Chip solve failed");
}
if (data.status === "success" && Array.isArray(data.solutions)) {
// THE FIX: Inject the specific baselines
const enhancedSolutions = data.solutions.map(sol => ({
...sol,
lockedBaselineEv,
paddedTotalEv: (sol.ev || 0) + (pastBaselineEv || 0)
}));
setChipSolveSolutions(enhancedSolutions);
} else {
alert("Chip solve failed: " + (data.message || "Unknown error"));
}
} catch (err) {
if (err.name === 'AbortError') return;
alert(err.message || "Failed to run chip solve.");
} finally {
setIsChipSolving(false);
}
};
const handleSensAnalysis = async ({
teamId, solveGWs, horizonGWs, teamData, globalPlayers, itb, availableFts,
quickSettings, chipsByGw, comprehensiveSettings, numSims, lockedBaselineEv, pastBaselineEv
}) => {
setIsRunningSens(true);
setSensResults(null);
abortControllerRef.current = new AbortController();
try {
const payload = {
team_id: parseInt(teamId, 10) || 0,
horizon_gws: solveGWs,
current_squad_ids: teamData.filter((p) => !p.isBlank && typeof p.ID === "number").map((p) => p.ID),
market_players: formatMarketPlayers(globalPlayers, teamData, horizonGWs),
in_the_bank: itb,
free_transfers: availableFts,
settings: buildBaseSettings(quickSettings, chipsByGw, 1), // Force 1 iteration for sens analysis
comprehensive_settings: comprehensiveSettings,
num_sims: numSims,
analysis_gw: solveGWs[0] || null,
};
const res = await fetch("https://anayshukla-fpl-solver.hf.space/api/sensitivity", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: abortControllerRef.current.signal,
});
const data = await res.json();
if (!res.ok) {
const d = data.detail;
const msg = typeof d === "string" ? d : Array.isArray(d) ? d.map((x) => x.msg || x).join(", ") : JSON.stringify(d);
throw new Error(msg || "Sensitivity analysis failed");
}
if (data.status === "success") {
setSensResults(data);
setSensViewGw(data.horizon_gws?.[0] ?? null);
} else {
alert("Sensitivity failed: " + (data.message || "Unknown error"));
}
} catch (err) {
if (err.name === 'AbortError') return;
alert(err.message || "Failed to run sensitivity analysis.");
} finally {
setIsRunningSens(false);
}
};
const loadSettingsFromCloud = async (teamId) => {
try {
const res = await fetch(`https://anayshukla-fpl-solver.hf.space/api/settings/${teamId}`);
const data = await res.json();
if (data.status === "success") {
console.log("📥 LOADED FROM CLOUD:", data);
return {
quick: data.quick_settings,
advanced: data.advanced_settings
};
}
} catch (err) {
console.warn("Failed to load cloud settings:", err);
}
return null;
};
const saveSettingsToCloud = async (teamId, quickSettings, compSettings) => {
console.log("📤 SAVING TO CLOUD. Advanced Settings payload:", compSettings);
try {
await fetch("https://anayshukla-fpl-solver.hf.space/api/settings/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
team_id: parseInt(teamId, 10),
quick_settings: quickSettings,
advanced_settings: compSettings // <-- Safely maps React state to Python expectation
})
});
} catch (err) {
console.warn("Failed to save settings to cloud:", err);
}
};
return {
isSolving,
isChipSolving,
isRunningSens,
pendingSolutions,
setPendingSolutions,
chipSolveSolutions,
setChipSolveSolutions,
sensResults,
setSensResults,
sensViewGw,
setSensViewGw,
handleSolve,
handleChipSolve,
handleSensAnalysis,
loadSettingsFromCloud,
saveSettingsToCloud,
};
};