import { useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertCircle, Cpu, Download, Loader2, RotateCw, Trash2, Zap } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { apiClient } from '@/lib/api/client'; import type { CudaDownloadProgress } from '@/lib/api/types'; import { useServerHealth } from '@/lib/hooks/useServer'; import { usePlatform } from '@/platform/PlatformContext'; import { useServerStore } from '@/stores/serverStore'; type RestartPhase = 'idle' | 'stopping' | 'waiting' | 'ready'; export function GpuAcceleration() { const platform = usePlatform(); const queryClient = useQueryClient(); const serverUrl = useServerStore((state) => state.serverUrl); const { data: health } = useServerHealth(); const [restartPhase, setRestartPhase] = useState('idle'); const [error, setError] = useState(null); const [downloadProgress, setDownloadProgress] = useState(null); const healthPollRef = useRef | null>(null); // Query CUDA backend status const { data: cudaStatus, isLoading: cudaStatusLoading, refetch: refetchCudaStatus, } = useQuery({ queryKey: ['cuda-status', serverUrl], queryFn: () => apiClient.getCudaStatus(), refetchInterval: cudaStatusLoading ? false : 10000, retry: 1, enabled: !!health, // Only fetch when backend is reachable }); // Derived state const isCurrentlyCuda = health?.backend_variant === 'cuda'; const cudaAvailable = cudaStatus?.available ?? false; const cudaDownloading = cudaStatus?.downloading ?? false; // Clean up health poll on unmount useEffect(() => { return () => { if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } }; }, []); // SSE progress tracking during download useEffect(() => { if (!cudaDownloading || !serverUrl) { return; } const eventSource = new EventSource(`${serverUrl}/backend/cuda-progress`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data) as CudaDownloadProgress; setDownloadProgress(data); if (data.status === 'complete') { eventSource.close(); setDownloadProgress(null); refetchCudaStatus(); } else if (data.status === 'error') { eventSource.close(); setError(data.error || 'Download failed'); setDownloadProgress(null); refetchCudaStatus(); } } catch (e) { console.error('Error parsing CUDA progress event:', e); } }; eventSource.onerror = () => { eventSource.close(); }; return () => { eventSource.close(); }; }, [cudaDownloading, serverUrl, refetchCudaStatus]); // Start aggressive health polling during restart const startHealthPolling = useCallback(() => { if (healthPollRef.current) return; healthPollRef.current = setInterval(async () => { try { const result = await apiClient.getHealth(); if (result.status === 'healthy') { // Server is back up if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } setRestartPhase('ready'); // Invalidate all queries to refresh UI queryClient.invalidateQueries(); // Reset after a moment setTimeout(() => setRestartPhase('idle'), 2000); } } catch { // Server still down, keep polling } }, 1000); }, [queryClient]); const handleDownload = async () => { setError(null); try { await apiClient.downloadCudaBackend(); refetchCudaStatus(); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Failed to start download'; if (msg.includes('already downloaded')) { refetchCudaStatus(); } else { setError(msg); } } }; const handleRestart = async () => { setError(null); setRestartPhase('stopping'); try { setRestartPhase('waiting'); startHealthPolling(); await platform.lifecycle.restartServer(); // Invoke resolved — server is likely ready. Stop polling and refresh. if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } setRestartPhase('ready'); queryClient.invalidateQueries(); setTimeout(() => setRestartPhase('idle'), 2000); } catch (e: unknown) { setRestartPhase('idle'); if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } setError(e instanceof Error ? e.message : 'Restart failed'); } }; const handleSwitchToCpu = async () => { // To switch to CPU: delete the CUDA binary, then restart. // start_server always prefers CUDA if present, so we must remove it first. setError(null); setRestartPhase('stopping'); try { await apiClient.deleteCudaBackend(); setRestartPhase('waiting'); startHealthPolling(); await platform.lifecycle.restartServer(); // Invoke resolved — server is likely ready if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } setRestartPhase('ready'); queryClient.invalidateQueries(); setTimeout(() => setRestartPhase('idle'), 2000); } catch (e: unknown) { setRestartPhase('idle'); if (healthPollRef.current) { clearInterval(healthPollRef.current); healthPollRef.current = null; } setError(e instanceof Error ? e.message : 'Failed to switch to CPU'); refetchCudaStatus(); } }; const handleDelete = async () => { setError(null); try { await apiClient.deleteCudaBackend(); refetchCudaStatus(); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to delete CUDA backend'); } }; const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`; }; // Don't render until health data is available if (!health) return null; // If the system already has native GPU (MPS, etc.), only show info - no CUDA needed const hasNativeGpu = health.gpu_available && !isCurrentlyCuda && health.gpu_type && !health.gpu_type.includes('CUDA'); return ( GPU Acceleration {/* Current status */}
Backend
{isCurrentlyCuda ? 'CUDA (GPU accelerated)' : 'CPU'}
{isCurrentlyCuda ? ( <> CUDA ) : ( <> CPU )}
{/* GPU info from health */} {health.gpu_type && (
GPU
{health.gpu_type}
{health.vram_used_mb != null && (
VRAM: {health.vram_used_mb.toFixed(0)} MB used
)}
)} {/* Native GPU detected - no CUDA download needed */} {hasNativeGpu && (
Your system uses {health.gpu_type} for acceleration. No additional downloads needed.
)} {/* CUDA download section - only show when native GPU is NOT detected (i.e., Windows/Linux NVIDIA users) */} {!hasNativeGpu && ( <> {/* Download progress */} {cudaDownloading && downloadProgress && (
{downloadProgress.filename || 'Downloading CUDA backend...'}
{downloadProgress.total > 0 && ( {downloadProgress.progress.toFixed(1)}% )}
{downloadProgress.total > 0 && ( <>
{formatBytes(downloadProgress.current)} /{' '} {formatBytes(downloadProgress.total)}
)}
)} {/* Restart in progress */} {restartPhase !== 'idle' && (
{restartPhase === 'stopping' && 'Stopping server...'} {restartPhase === 'waiting' && 'Restarting server...'} {restartPhase === 'ready' && 'Server restarted successfully!'}
)} {/* Error display */} {error && (
{error}
)} {/* Actions */} {restartPhase === 'idle' && !cudaDownloading && (
{/* Not downloaded yet - show download button */} {!cudaAvailable && (

Download the CUDA backend (~2.4 GB) for NVIDIA GPU acceleration. Requires an NVIDIA GPU with CUDA support.

)} {/* Downloaded but not active - show switch button */} {cudaAvailable && !isCurrentlyCuda && platform.metadata.isTauri && (

CUDA backend is downloaded and ready. Restart the server to enable GPU acceleration.

)} {/* Currently active - show switch back to CPU */} {isCurrentlyCuda && platform.metadata.isTauri && (

Running with CUDA GPU acceleration. Switch back to CPU if needed (you can re-download later).

)} {/* Delete option when downloaded (and not active) */} {cudaAvailable && !isCurrentlyCuda && ( )}
)} )}
); }