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