import { useState, useEffect, useRef } from "react"; import { Save, Trash2, Check, ChevronRight, Pencil, Star, Fan, } from "lucide-react"; import { api } from "../../api/client.js"; import type { ProfileData } from "../../types.js"; import { toast } from "sonner"; import { useCurveStore } from "../../store/curveStore.js"; function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e); } interface ProfilePanelProps { activeProfile: string | null; onProfileApplied: (name: string | null) => void; } export function ProfilePanel({ activeProfile, onProfileApplied, }: ProfilePanelProps) { const { selectedGpuIndex, gpuInfo } = useCurveStore(); const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [autoLoadProfile, setAutoLoadProfile] = useState(null); // Save form const [isSaveOpen, setIsSaveOpen] = useState(false); const [newName, setNewName] = useState(""); const [isSaving, setIsSaving] = useState(false); const saveInputRef = useRef(null); // Inline delete confirmation const [deletingName, setDeletingName] = useState(null); const [isDeleting, setIsDeleting] = useState(false); // Inline rename const [renamingName, setRenamingName] = useState(null); const [renameValue, setRenameValue] = useState(""); const [isRenaming, setIsRenaming] = useState(false); const renameInputRef = useRef(null); // Apply in-flight const [applyingName, setApplyingName] = useState(null); async function fetchProfiles() { try { const data = await api.profiles(selectedGpuIndex); setProfiles(data.profiles); onProfileApplied(data.active); setAutoLoadProfile(data.auto_load); } catch { toast.error("Failed to load profiles"); } finally { setLoading(false); } } async function handleSetAutoLoad(name: string | null) { try { await api.setAutoLoadProfile(name, selectedGpuIndex); setAutoLoadProfile(name); if (name) toast.success(`"${name}" will load on server start`); else toast.success("Auto-load cleared"); } catch (e: unknown) { toast.error("Failed to update default profile: " + errMsg(e)); } } // 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 fetchProfiles(); }, [selectedGpuIndex]); useEffect(() => { if (isSaveOpen) saveInputRef.current?.focus(); }, [isSaveOpen]); useEffect(() => { if (renamingName) renameInputRef.current?.focus(); }, [renamingName]); function closeSaveForm() { setIsSaveOpen(false); setNewName(""); } async function handleSave(e: React.SubmitEvent) { e.preventDefault(); if (!newName.trim()) return; try { setIsSaving(true); await api.saveProfile(newName.trim(), selectedGpuIndex); toast.success(`Profile "${newName.trim()}" saved`); closeSaveForm(); await fetchProfiles(); } catch (e: unknown) { toast.error("Failed to save: " + errMsg(e)); } finally { setIsSaving(false); } } async function handleApply(name: string) { try { setApplyingName(name); await api.applyProfile(name, selectedGpuIndex); onProfileApplied(name); toast.success(`"${name}" applied`); } catch (e: unknown) { toast.error(`Failed to apply "${name}": ` + errMsg(e)); } finally { setApplyingName(null); } } async function handleDeleteConfirm(name: string) { try { setIsDeleting(true); await api.deleteProfile(name); toast.success(`"${name}" deleted`); if (activeProfile === name) onProfileApplied(null); if (autoLoadProfile === name) setAutoLoadProfile(null); setDeletingName(null); await fetchProfiles(); } catch (e: unknown) { toast.error("Failed to delete: " + errMsg(e)); } finally { setIsDeleting(false); } } async function handleRename(e: React.SubmitEvent, oldName: string) { e.preventDefault(); if (!renameValue.trim() || renameValue.trim() === oldName) { setRenamingName(null); return; } try { setIsRenaming(true); await api.renameProfile(oldName, renameValue.trim()); toast.success(`Renamed to "${renameValue.trim()}"`); if (activeProfile === oldName) onProfileApplied(renameValue.trim()); if (autoLoadProfile === oldName) setAutoLoadProfile(renameValue.trim()); setRenamingName(null); await fetchProfiles(); } catch (e: unknown) { toast.error("Failed to rename: " + errMsg(e)); } finally { setIsRenaming(false); } } if (loading && profiles.length === 0) { return (
); } return (
{/* Header */}

Profiles

{/* Save section */}
{isSaveOpen ? (
setNewName(e.target.value)} disabled={isSaving} onKeyDown={(e) => e.key === "Escape" && closeSaveForm()} className="flex-1 min-w-0 bg-zinc-950 border border-zinc-700 rounded px-3 py-1.5 text-sm focus:outline-none focus:border-pink-500 focus:ring-1 focus:ring-pink-500 disabled:opacity-50" />
) : ( )}
{/* Profile list */}
{profiles.length === 0 ? (

No profiles saved yet.

) : (
{profiles.map((p) => { const pts = Object.keys(p.curve_deltas).length; const badges = [ pts > 0 ? `${pts} pts` : null, p.power_limit_w != null ? `${p.power_limit_w}W` : null, p.mem_offset_mhz != null ? `${p.mem_offset_mhz > 0 ? "+" : ""}${p.mem_offset_mhz} MHz mem` : null, ] .filter(Boolean) .join(" · "); const isActive = activeProfile === p.name; const isAutoLoad = autoLoadProfile === p.name; const isApplying = applyingName === p.name; const isConfirmingDelete = deletingName === p.name; const isRenaming_ = renamingName === p.name; return (
{isConfirmingDelete ? (
Delete "{p.name}"?
) : isRenaming_ ? (
handleRename(e, p.name)} className="flex items-center gap-2 px-3 py-2" > setRenameValue(e.target.value)} disabled={isRenaming} onKeyDown={(e) => e.key === "Escape" && setRenamingName(null) } className="flex-1 min-w-0 bg-zinc-950 border border-zinc-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-pink-500 focus:ring-1 focus:ring-pink-500 disabled:opacity-50" />
) : (
{isActive ? ( ) : ( )}

{p.name}

{isAutoLoad && ( )} {p.fan_curve && p.fan_curve.length > 0 && ( )} {gpuInfo && p.gpu_name !== gpuInfo.name && ( {p.gpu_name} )}
{badges && (

{badges}

)} {p.power_cap_mode === "ioctl" && (

⚠ experimental power (below VBIOS min)

)}
)}
); })}
)}
); }