import { useState, useEffect } from "react"; import { Check, X, RotateCcw } from "lucide-react"; import { api } from "../../api/client.js"; import { useCurveStore } from "../../store/curveStore.js"; import type { LimitsState } from "../../types.js"; import { toast } from "sonner"; import { ConfirmDialog } from "../common/ConfirmDialog.js"; type Pending = Pick; export function PerformancePanel() { const { selectedGpuIndex } = useCurveStore(); const [limits, setLimits] = useState(null); const [pending, setPending] = useState>({}); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [confirmApply, setConfirmApply] = useState(false); const [confirmReset, setConfirmReset] = useState(false); async function fetchLimits() { try { setLoading(true); setLimits(await api.limits(selectedGpuIndex)); } catch { toast.error("Failed to load performance limits"); } finally { setLoading(false); } } // Data fetch on GPU change — setState calls happen after the await, not // synchronously in the effect body (rule false-positive on async fetch). useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchLimits(); }, [selectedGpuIndex]); async function handleApply() { setBusy(true); setError(null); try { await api.updateLimits(pending, selectedGpuIndex); useCurveStore.getState().setActiveProfile(null); setPending({}); setConfirmApply(false); await fetchLimits(); toast.success("Performance limits applied"); } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)); setConfirmApply(false); } finally { setBusy(false); } } async function handleReset() { setBusy(true); setError(null); try { await api.resetLimits(selectedGpuIndex); useCurveStore.getState().setActiveProfile(null); setPending({}); setConfirmReset(false); await fetchLimits(); toast.success("Performance limits reset to defaults"); } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)); setConfirmReset(false); } finally { setBusy(false); } } if (loading && !limits) { return (
); } if (!limits) return null; const hasPending = Object.keys(pending).length > 0; // Power const pwrVal = pending.power_limit_w ?? limits.power_limit_w ?? 0; const pwrMin = limits.min_power_limit_w ?? 100; const pwrMax = limits.max_power_limit_w ?? 600; // Memory offset const memVal = pending.mem_offset_mhz ?? limits.mem_offset_mhz ?? 0; const memMin = limits.min_mem_offset_mhz ?? -2000; const memMax = Math.min(limits.max_mem_offset_mhz ?? 3000, 3000); return ( <>
{/* ── Header ─────────────────────────────────────────────────────── */}
Performance {hasPending && ( {Object.keys(pending).length} pending )}
{/* ── Error banner ────────────────────────────────────────────────── */} {error && (
⚠ {error}
)}
{/* ── Board Power Limit ─────────────────────────────────────────── */}
Board Power Limit
{ const v = parseInt(e.target.value); if (!isNaN(v)) setPending((p) => ({ ...p, power_limit_w: v })); }} className="w-14 bg-zinc-950 border border-zinc-800 rounded text-xs px-2 py-1 text-right font-mono focus:outline-none focus:border-cyan-500" /> W
{pwrMin} setPending((p) => ({ ...p, power_limit_w: parseInt(e.target.value), })) } className="flex-1 accent-cyan-400 h-1 cursor-pointer" /> {pwrMax}
{/* ── Memory Clock Offset ───────────────────────────────────────── */}
Memory Clock Offset
{ const v = parseInt(e.target.value); if (!isNaN(v)) setPending((p) => ({ ...p, mem_offset_mhz: v })); }} className="w-16 bg-zinc-950 border border-zinc-800 rounded text-xs px-2 py-1 text-right font-mono focus:outline-none focus:border-cyan-500" /> MHz
{memMin} setPending((p) => ({ ...p, mem_offset_mhz: parseInt(e.target.value), })) } className="flex-1 accent-cyan-400 h-1 cursor-pointer" /> +{memMax}
{confirmApply && ( setConfirmApply(false)} /> )} {confirmReset && ( setConfirmReset(false)} /> )} ); }