import type { FeatureBundleState } from "@snapotter/shared"; import { Clock, Download, Loader2, RefreshCw, RotateCcw, Trash2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { apiGet } from "@/lib/api"; import { useFeaturesStore } from "@/stores/features-store"; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } function formatTimeRemaining(ms: number): string { if (ms < 60000) return "Less than a minute left"; const mins = Math.ceil(ms / 60000); if (mins === 1) return "~1 minute left"; return `~${mins} minutes left`; } const PROGRESS_MESSAGES = [ "Almost there... probably...", "Good things take time...", "Still faster than watching paint dry...", "Your patience is truly inspiring...", "Working harder than it looks...", "This is the exciting part, trust me...", "Doing important behind-the-scenes stuff...", "If you're reading this, it's working...", "Preparing something awesome...", "Worth every second, pinky promise...", "The suspense is part of the experience...", "Teaching your computer new tricks...", "Setting up your superpowers...", "Your images will thank you later...", "Loading... but make it fancy...", "This would be a great time for coffee...", "Rome wasn't built in a day either...", "Shhh... genius at work...", "Making your photos jealous of what's coming...", "Assembling the dream team...", "Unpacking awesomeness...", "Almost done thinking about starting... just kidding...", "Plot twist: this is actually doing something...", "Warming up the creative engines...", "Imagination loading...", "Not a screensaver, we promise...", "Great art takes time to install...", "Your future self will thank you...", "Grabbing some really smart files...", "Hang tight, the best is yet to come...", ]; export function AiFeaturesSection() { const { bundles, fetch, installing, errors, queued, installAllActive, startTimes, installBundle, uninstallBundle, reinstallBundle, installAll, } = useFeaturesStore(); const [diskUsage, setDiskUsage] = useState(null); const loadDiskUsage = useCallback(async () => { try { const data = await apiGet<{ totalBytes: number }>("/v1/admin/features/disk-usage"); setDiskUsage(data.totalBytes); } catch {} }, []); useEffect(() => { fetch(); loadDiskUsage(); }, [fetch, loadDiskUsage]); const prevInstallingKeys = useRef(new Set(Object.keys(installing))); useEffect(() => { const currentKeys = new Set(Object.keys(installing)); for (const key of prevInstallingKeys.current) { if (!currentKeys.has(key)) { loadDiskUsage(); break; } } prevInstallingKeys.current = currentKeys; }, [installing, loadDiskUsage]); return (

AI Features

Manage AI model bundles for advanced image processing.

{bundles.map((bundle) => ( installBundle(bundle.id)} onUninstall={() => uninstallBundle(bundle.id)} onReinstall={() => reinstallBundle(bundle.id)} isInstalling={!!installing[bundle.id]} isQueued={queued.includes(bundle.id)} startTime={startTimes[bundle.id] ?? null} /> ))}
{diskUsage !== null && (

Disk usage: {formatBytes(diskUsage)}

)}
); } interface BundleProgress { percent: number; stage: string; } function BundleCard({ bundle, progress, error, onInstall, onUninstall, onReinstall, isInstalling, isQueued, startTime, }: { bundle: FeatureBundleState; progress: BundleProgress | null; error: string | null; onInstall: () => void; onUninstall: () => void; onReinstall: () => void; isInstalling: boolean; isQueued: boolean; startTime: number | null; }) { const [confirming, setConfirming] = useState(false); const [messageIndex, setMessageIndex] = useState(() => Math.floor(Math.random() * PROGRESS_MESSAGES.length), ); const [now, setNow] = useState(Date.now()); const status = isQueued ? "queued" : isInstalling ? "installing" : bundle.status; useEffect(() => { if (!isInstalling) return; const interval = setInterval(() => { setMessageIndex((prev) => (prev + 1) % PROGRESS_MESSAGES.length); setNow(Date.now()); }, 3000); return () => clearInterval(interval); }, [isInstalling]); const eta = (() => { if (!progress || !startTime || progress.percent <= 2) return null; const elapsed = now - startTime; const rate = progress.percent / elapsed; if (rate <= 0) return null; const remaining = (100 - progress.percent) / rate; return formatTimeRemaining(remaining); })(); return (

{bundle.name}

{bundle.description} (~{bundle.estimatedSize})

{status === "installed" && ( <> Installed )} {status === "not_installed" && !error && ( <> Not installed )} {status === "queued" && ( <> Queued )} {status === "installing" && progress && ( <> {progress.percent}% )} {(status === "error" || error) && ( <> {error ?? bundle.error} )}
{status === "not_installed" && !error && ( )} {status === "installed" && !confirming && (
)} {status === "installed" && confirming && (
)} {status === "installing" && ( )} {(status === "error" || error) && !isInstalling && !isQueued && ( )}
{status === "installing" && progress && (

{PROGRESS_MESSAGES[messageIndex]}

{eta &&

{eta}

}
)}
); }