Files
nvcurve/frontend/src/App.tsx
T

160 lines
6.8 KiB
TypeScript

import { useGpu } from './hooks/useGpu';
import { useCurve } from './hooks/useCurve';
import { useMonitor } from './hooks/useMonitor';
import { StatusBar } from './components/Monitor/StatusBar';
import { LiveMonitor } from './components/Monitor/LiveMonitor';
import { CurveEditor } from './components/CurveEditor/CurveEditor';
import { PointTable } from './components/PointTable/PointTable';
import { PerformancePanel } from './components/Limits/PerformancePanel';
import { PerformanceMonitor } from './components/Monitor/PerformanceMonitor';
import { ProfilePanel } from './components/Profiles/ProfilePanel';
import { api } from './api/client';
import { useCurveStore } from './store/curveStore';
import { Toaster } from 'sonner';
import { Loader, ChevronDown } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
export default function App() {
const gpuInfo = useGpu();
const { curve, wsStatus: curveWsStatus } = useCurve();
const { monitor, monitorHistory, wsStatus: monitorWsStatus } = useMonitor();
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } = useCurveStore();
const [activeTab, setActiveTab] = useState<'curve' | 'performance'>('curve');
const [activeDomain, setActiveDomain] = useState<'gpu' | 'memory'>('gpu');
const [isProfileOpen, setIsProfileOpen] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
const currentVoltageMv = monitor?.voltage_mv ?? null;
function handleRefresh() {
api.curve(selectedGpuIndex).then(setCurve).catch(console.error);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (profileRef.current && !profileRef.current.contains(event.target as Node)) {
setIsProfileOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
// Fetch initial active profile
api.profiles(selectedGpuIndex).then(data => setActiveProfile(data.active)).catch(console.error);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [selectedGpuIndex, setActiveProfile]);
// Worst connection status wins
const wsStatus =
monitorWsStatus === 'disconnected' || curveWsStatus === 'disconnected'
? 'disconnected'
: monitorWsStatus === 'connecting' || curveWsStatus === 'connecting'
? 'connecting'
: 'connected';
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100 flex flex-col">
<Toaster theme="dark" position="top-right" />
<StatusBar gpuInfo={gpuInfo} wsStatus={wsStatus} monitor={monitor} />
{wsStatus !== 'connected' && (
<div className="bg-amber-500/10 border-b border-amber-500/20 text-amber-500 px-4 py-2 text-center text-sm font-medium flex justify-center items-center gap-2">
<Loader size={14} className="animate-spin" />
{wsStatus === 'connecting' ? 'Reconnecting to backend...' : 'Connection lost. Retrying...'}
</div>
)}
<main className="flex-1 p-4 flex flex-col gap-4 w-full max-w-[1600px] mx-auto">
{/* Tab Header and Profile Selector */}
<div className="flex justify-between items-end border-b border-zinc-800 pb-2">
<div className="flex gap-6">
<button
onClick={() => setActiveTab('curve')}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === 'curve' ? 'border-pink-500 text-zinc-100' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
>
Curve
</button>
<button
onClick={() => setActiveTab('performance')}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === 'performance' ? 'border-pink-500 text-zinc-100' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
>
Performance
</button>
</div>
<div className="relative" ref={profileRef}>
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center gap-2 text-zinc-400 hover:text-zinc-100 transition-colors text-sm font-medium pb-2 -mb-[9px]"
>
{activeProfile ? (
<>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 shrink-0" />
<span className="text-zinc-200">{activeProfile}</span>
</>
) : (
'Profiles'
)}
<ChevronDown size={14} className={isProfileOpen ? "rotate-180 transition-transform" : "transition-transform"} />
</button>
{isProfileOpen && (
<div className="absolute right-0 top-full mt-2 w-80 z-50 shadow-[0_8px_30px_rgb(0,0,0,0.8)] border border-zinc-700/50 rounded-lg overflow-hidden">
<ProfilePanel activeProfile={activeProfile} onProfileApplied={setActiveProfile} />
</div>
)}
</div>
</div>
{/* Main content area */}
<div className="flex flex-col gap-4 w-full">
{activeTab === 'curve' ? (
<>
<div className="flex gap-4 items-stretch w-full">
<div className="flex-1 min-w-0 flex flex-col">
{curve ? (
<CurveEditor
curve={curve}
activeDomain={activeDomain}
onDomainChange={setActiveDomain}
currentVoltageMv={currentVoltageMv}
currentClockMhz={monitor?.clock_mhz ?? null}
onRefresh={handleRefresh}
/>
) : (
<div className="bg-zinc-900 rounded-lg p-8 text-center text-zinc-500 h-full flex items-center justify-center">
Loading curve…
</div>
)}
</div>
<div className="w-80 shrink-0 flex flex-col">
<LiveMonitor monitor={monitor} history={monitorHistory} />
</div>
</div>
{curve && (
<div className="w-full">
<PointTable
points={curve.points.filter(p => p.domain === activeDomain)}
currentVoltageMv={activeDomain === 'gpu' ? currentVoltageMv : null}
readOnly={activeDomain === 'memory'}
/>
</div>
)}
</>
) : (
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">
<PerformancePanel />
</div>
<div className="w-80 shrink-0 flex flex-col">
<PerformanceMonitor monitor={monitor} history={monitorHistory} />
</div>
</div>
)}
</div>
</main>
</div>
);
}