import { useEffect, useMemo, useRef, useState } from "react"; import { CleanupOrphanedThumbnailsResult, DatabaseInfo, OrphanedThumbnailsInfo, TaggerAcceleration, TaggingQueueScope, VacuumResult, useGalleryStore } from "../store"; type SettingsSection = "workspace" | "general"; const SECTIONS: { id: SettingsSection; label: string; detail: string }[] = [ { id: "workspace", label: "AI Workspace", detail: "Tagging models and queue targets" }, { id: "general", label: "General", detail: "App data and diagnostics" }, ]; function StatusPill({ children, tone }: { children: React.ReactNode; tone: "ready" | "muted" | "busy" }) { const className = tone === "ready" ? "border-emerald-400/25 bg-emerald-500/10 text-emerald-300" : tone === "busy" ? "border-sky-400/25 bg-sky-500/10 text-sky-300" : "border-white/10 bg-white/[0.04] text-gray-500"; return {children}; } function SectionShell({ eyebrow, title, description, children }: { eyebrow: string; title: string; description?: string; children: React.ReactNode; }) { return (

{eyebrow}

{title}

{description ?

{description}

: null}
{children}
); } function SettingsCard({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) { return (

{title}

{description ?

{description}

: null}
{children}
); } function SettingsRow({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { return (

{title}

{description}

{children}
); } function ScopeButton({ scope, current, onSelect, children }: { scope: TaggingQueueScope; current: TaggingQueueScope; onSelect: (scope: TaggingQueueScope) => void; children: React.ReactNode; }) { const active = scope === current; return ( ); } function TaggerAccelerationButton({ acceleration, current, onSelect, children }: { acceleration: TaggerAcceleration; current: TaggerAcceleration; onSelect: (acceleration: TaggerAcceleration) => void; children: React.ReactNode; }) { const active = acceleration === current; return ( ); } export function SettingsModal() { const [activeSection, setActiveSection] = useState("workspace"); const [taggerQueueStatus, setTaggerQueueStatus] = useState(null); const [taggerQueueing, setTaggerQueueing] = useState(false); const [taggerClearing, setTaggerClearing] = useState(false); const [taggerAccelerationSaving, setTaggerAccelerationSaving] = useState(false); const [taggerAccelerationError, setTaggerAccelerationError] = useState(null); const [taggerThresholdDraft, setTaggerThresholdDraft] = useState(null); const [taggerThresholdSaving, setTaggerThresholdSaving] = useState(false); const [taggerThresholdError, setTaggerThresholdError] = useState(null); const [taggerBatchSizeDraft, setTaggerBatchSizeDraft] = useState(null); const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false); const [taggerBatchSizeError, setTaggerBatchSizeError] = useState(null); const [openingDataFolder, setOpeningDataFolder] = useState(false); const [dbInfo, setDbInfo] = useState(null); const [vacuuming, setVacuuming] = useState(false); const [vacuumResult, setVacuumResult] = useState(null); const [thumbnailInfo, setThumbnailInfo] = useState(null); const [cleaningThumbnails, setCleaningThumbnails] = useState(false); const [thumbnailCleanupResult, setThumbnailCleanupResult] = useState(null); const thresholdErrorTimerRef = useRef | null>(null); const batchSizeErrorTimerRef = useRef | null>(null); const settingsOpen = useGalleryStore((state) => state.settingsOpen); const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen); const folders = useGalleryStore((state) => state.folders); const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress); const taggingQueueScope = useGalleryStore((state) => state.taggingQueueScope); const taggingQueueFolderIds = useGalleryStore((state) => state.taggingQueueFolderIds); const loadTaggingQueueScope = useGalleryStore((state) => state.loadTaggingQueueScope); const setTaggingQueueScope = useGalleryStore((state) => state.setTaggingQueueScope); const toggleTaggingQueueFolder = useGalleryStore((state) => state.toggleTaggingQueueFolder); const setTaggingQueueFolderIds = useGalleryStore((state) => state.setTaggingQueueFolderIds); const loadTaggingQueueFolderIds = useGalleryStore((state) => state.loadTaggingQueueFolderIds); const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus); const taggerModelPreparing = useGalleryStore((state) => state.taggerModelPreparing); const taggerModelProgress = useGalleryStore((state) => state.taggerModelProgress); const taggerModelError = useGalleryStore((state) => state.taggerModelError); const taggerAcceleration = useGalleryStore((state) => state.taggerAcceleration); const taggerThreshold = useGalleryStore((state) => state.taggerThreshold); const taggerBatchSize = useGalleryStore((state) => state.taggerBatchSize); const taggerRuntimeProbe = useGalleryStore((state) => state.taggerRuntimeProbe); const taggerRuntimeChecking = useGalleryStore((state) => state.taggerRuntimeChecking); const loadTaggerModelStatus = useGalleryStore((state) => state.loadTaggerModelStatus); const prepareTaggerModel = useGalleryStore((state) => state.prepareTaggerModel); const deleteTaggerModel = useGalleryStore((state) => state.deleteTaggerModel); const loadTaggerAcceleration = useGalleryStore((state) => state.loadTaggerAcceleration); const setTaggerAcceleration = useGalleryStore((state) => state.setTaggerAcceleration); const loadTaggerThreshold = useGalleryStore((state) => state.loadTaggerThreshold); const setTaggerThreshold = useGalleryStore((state) => state.setTaggerThreshold); const loadTaggerBatchSize = useGalleryStore((state) => state.loadTaggerBatchSize); const setTaggerBatchSize = useGalleryStore((state) => state.setTaggerBatchSize); const probeTaggerRuntime = useGalleryStore((state) => state.probeTaggerRuntime); const queueTaggingJobs = useGalleryStore((state) => state.queueTaggingJobs); const queueTaggingJobsForFolders = useGalleryStore((state) => state.queueTaggingJobsForFolders); const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs); const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders); const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder); const getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo); const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase); const getOrphanedThumbnailsInfo = useGalleryStore((state) => state.getOrphanedThumbnailsInfo); const cleanupOrphanedThumbnails = useGalleryStore((state) => state.cleanupOrphanedThumbnails); useEffect(() => { if (!settingsOpen) return; void loadTaggerModelStatus(); void loadTaggerAcceleration(); void loadTaggerThreshold(); void loadTaggerBatchSize(); void loadTaggingQueueScope(); void loadTaggingQueueFolderIds(); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setSettingsOpen(false); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, loadTaggingQueueScope, loadTaggingQueueFolderIds, setSettingsOpen]); useEffect(() => { if (!settingsOpen || activeSection !== "general") return; setVacuumResult(null); setThumbnailCleanupResult(null); void getDatabaseInfo().then(setDbInfo).catch(() => {}); void getOrphanedThumbnailsInfo().then(setThumbnailInfo).catch(() => {}); }, [settingsOpen, activeSection, getDatabaseInfo, getOrphanedThumbnailsInfo]); // Clean up error timers on unmount useEffect(() => { return () => { if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current); if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current); }; }, []); const selectedFolders = useMemo( () => folders.filter((folder) => taggingQueueFolderIds.includes(folder.id)), [folders, taggingQueueFolderIds], ); if (!settingsOpen) return null; const taggerReady = taggerModelStatus?.ready ?? false; const queueScopeLabel = taggingQueueScope === "all" ? "all media" : selectedFolders.length > 0 ? `${selectedFolders.length} selected folder${selectedFolders.length === 1 ? "" : "s"}` : "no folders selected"; const thresholdDisplay = taggerThresholdDraft ?? String(taggerThreshold); const batchSizeDisplay = taggerBatchSizeDraft ?? String(taggerBatchSize); const taggerDownloadLabel = taggerModelProgress ? `Downloading ${taggerModelProgress.completed_files}/${taggerModelProgress.total_files}` : taggerModelPreparing ? "Preparing WD Tagger..." : taggerReady ? "Installed" : "Install model"; const taggerDownloadPercent = taggerModelProgress ? Math.round((taggerModelProgress.completed_files / Math.max(taggerModelProgress.total_files, 1)) * 100) : 0; const runQueueAction = (action: "queue" | "clear") => { const selectedIds = taggingQueueFolderIds; const perform = taggingQueueScope === "all" ? action === "queue" ? queueTaggingJobs(null) : clearTaggingJobs(null) : selectedIds.length > 0 ? action === "queue" ? queueTaggingJobsForFolders(selectedIds) : clearTaggingJobsForFolders(selectedIds) : Promise.resolve(0); if (action === "queue") { setTaggerQueueing(true); } else { setTaggerClearing(true); } setTaggerQueueStatus(null); void perform .then((count) => { if (taggingQueueScope === "selected" && selectedIds.length === 0) { setTaggerQueueStatus("Choose at least one folder before running tagging jobs."); return; } setTaggerQueueStatus( count === 0 ? action === "queue" ? "No missing tags found for the current target." : "No queued tagging jobs to clear for the current target." : action === "queue" ? `Queued ${count.toLocaleString()} image${count === 1 ? "" : "s"} for tagging.` : `Cleared ${count.toLocaleString()} queued tagging job${count === 1 ? "" : "s"}.`, ); }) .catch((error) => setTaggerQueueStatus(String(error))) .finally(() => { if (action === "queue") { setTaggerQueueing(false); } else { setTaggerClearing(false); } }); }; return (
setSettingsOpen(false)}>
event.stopPropagation()} >

{activeSection === "workspace" ? "AI Workspace" : "General"}

{activeSection === "workspace" ? "Model setup and queue targets" : "App data and diagnostics"}

{activeSection === "workspace" ? (

WD SwinV2 Tagger v3

{taggerReady ? "Installed" : taggerModelPreparing ? "Preparing" : "Not installed"}

Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds.

{taggerReady ? ( <> ) : null}
{(["auto", "directml", "cpu"] as const).map((acceleration) => ( { setTaggerAccelerationSaving(true); setTaggerAccelerationError(null); void setTaggerAcceleration(nextAcceleration) .catch((error: unknown) => setTaggerAccelerationError(String(error))) .finally(() => setTaggerAccelerationSaving(false)); }} > {acceleration === "directml" ? "DirectML" : acceleration === "cpu" ? "CPU" : "Auto"} ))}
{taggerAccelerationError ? (

{taggerAccelerationError}

) : (

{taggerAccelerationSaving ? "Saving..." : `Current: ${taggerAcceleration}`}

)}
setTaggerThresholdDraft(event.target.value)} onBlur={() => { const value = parseFloat(thresholdDisplay); if (!isNaN(value) && value >= 0.05 && value <= 0.99) { setTaggerThresholdError(null); setTaggerThresholdSaving(true); void setTaggerThreshold(value) .catch((error: unknown) => setTaggerThresholdError(String(error))) .finally(() => { setTaggerThresholdDraft(null); setTaggerThresholdSaving(false); }); } else { setTaggerThresholdDraft(null); setTaggerThresholdError("Must be 0.05 – 0.99"); if (thresholdErrorTimerRef.current) clearTimeout(thresholdErrorTimerRef.current); thresholdErrorTimerRef.current = setTimeout(() => setTaggerThresholdError(null), 2000); } }} /> {taggerThresholdError ? (

{taggerThresholdError}

) : (

{taggerThresholdSaving ? "Saving..." : "Default: 0.35"}

)}
setTaggerBatchSizeDraft(event.target.value)} onBlur={() => { const value = parseInt(batchSizeDisplay, 10); if (!isNaN(value) && value >= 1 && value <= 100) { setTaggerBatchSizeError(null); setTaggerBatchSizeSaving(true); void setTaggerBatchSize(value) .catch((error: unknown) => setTaggerQueueStatus(String(error))) .finally(() => { setTaggerBatchSizeDraft(null); setTaggerBatchSizeSaving(false); }); } else { setTaggerBatchSizeDraft(null); setTaggerBatchSizeError("Must be 1 – 100"); if (batchSizeErrorTimerRef.current) clearTimeout(batchSizeErrorTimerRef.current); batchSizeErrorTimerRef.current = setTimeout(() => setTaggerBatchSizeError(null), 2000); } }} /> {taggerBatchSizeError ? (

{taggerBatchSizeError}

) : (

{taggerBatchSizeSaving ? "Saving..." : "Default: 8"}

)}

Model location

{taggerReady ? taggerModelStatus?.local_dir : "Not downloaded"}

{taggerModelProgress?.current_file ?

{taggerModelProgress.current_file}

: null} {taggerModelError ?

{taggerModelError}

: null} {taggerRuntimeProbe ? (

Runtime check

Ready

Tagger acceleration: {taggerRuntimeProbe.acceleration}

{taggerRuntimeProbe.session.file}

) : null}
All media Selected folders

Folder selection

Current target: {queueScopeLabel}.

{folders.map((folder) => { const active = taggingQueueFolderIds.includes(folder.id); const progress = mediaJobProgress[folder.id]; return ( ); })} {folders.length === 0 ?

Add a folder first to enable targeted tagging queues.

: null}
{taggerQueueStatus ?

{taggerQueueStatus}

: null}
) : (

Open the app data folder in Explorer to inspect or back up files.

Database size

{vacuumResult ? `${vacuumResult.after_mb.toFixed(1)} MB` : dbInfo ? `${dbInfo.size_mb.toFixed(1)} MB` : "—"}

Reclaimable

{vacuumResult ? `−${vacuumResult.freed_mb.toFixed(1)} MB freed` : dbInfo ? `${dbInfo.reclaimable_mb.toFixed(1)} MB` : "—"}

{vacuumResult ? `Compacted from ${vacuumResult.before_mb.toFixed(1)} MB to ${vacuumResult.after_mb.toFixed(1)} MB.` : dbInfo && dbInfo.reclaimable_mb < 0.5 ? "Database is already compact." : "Run this after removing folders or bulk-deleting images."}

Orphaned files

{thumbnailCleanupResult ? thumbnailCleanupResult.deleted_count.toLocaleString() : thumbnailInfo ? thumbnailInfo.count.toLocaleString() : "—"}

Reclaimable

{thumbnailCleanupResult ? `−${thumbnailCleanupResult.freed_mb.toFixed(1)} MB freed` : thumbnailInfo ? `${thumbnailInfo.size_mb.toFixed(1)} MB` : "—"}

{thumbnailCleanupResult ? `Removed ${thumbnailCleanupResult.deleted_count.toLocaleString()} orphaned thumbnail${thumbnailCleanupResult.deleted_count === 1 ? "" : "s"}.` : thumbnailInfo && thumbnailInfo.count === 0 ? "No orphaned thumbnails found." : "Remove thumbnails no longer associated with any indexed image."}

)}
); }