'use client'; import { useState, useCallback } from 'react'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { UsageDashboard } from './usage-dashboard'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, } from '@/components/ui/alert-dialog'; import { Loader2, Trash2, AlertTriangle } from 'lucide-react'; import { useI18n } from '@/lib/hooks/use-i18n'; import { clearDatabase } from '@/lib/utils/database'; import { useSettingsStore } from '@/lib/store/settings'; import { useUserProfileStore } from '@/lib/store/user-profile'; import { toast } from 'sonner'; import { createLogger } from '@/lib/logger'; import { clearCacheErrorMessage } from './clear-cache-error-message'; import { runClearCache, shouldReloadAfterClear } from './clear-cache-workflow'; const log = createLogger('GeneralSettings'); /** * The shape of a zustand `persist` API this file needs. Declared structurally * so one helper covers both stores without importing either state type. */ interface PersistApi { getOptions: () => { name?: string; storage?: { removeItem: (name: string) => unknown }; }; } /** * Clear a store that persists through the KVStore. * * Not `persist.clearStorage()`: that discards the promise our KV-backed storage * returns, and clearing has to be awaited before the reload below. */ async function clearPersistedStore(persistApi: PersistApi, fallbackName: string): Promise { const { storage, name } = persistApi.getOptions(); await storage?.removeItem(name ?? fallbackName); } export function GeneralSettings() { const { t } = useI18n(); // Clear cache state const [showClearDialog, setShowClearDialog] = useState(false); const [confirmInput, setConfirmInput] = useState(''); const [clearing, setClearing] = useState(false); const confirmPhrase = t('settings.clearCacheConfirmPhrase'); const isConfirmValid = confirmInput === confirmPhrase; const handleClearCache = useCallback(async () => { if (!isConfirmValid) return; setClearing(true); try { const result = await runClearCache({ clearDatabase, clearLocalStorage: () => localStorage.clear(), clearSessionStorage: () => sessionStorage.clear(), clearPersistedStores: async () => { // The blanket clear only reaches these stores while their KV backend // happens to use localStorage. Account-scoped storage needs explicit cleanup. await Promise.all([ clearPersistedStore(useSettingsStore.persist, 'settings-storage'), clearPersistedStore(useUserProfileStore.persist, 'user-profile-storage'), ]); }, }); if (result.status === 'asset-pool-deferred') { log.warn('Asset pool deletion deferred; remaining cache cleanup completed.'); toast.error(clearCacheErrorMessage(result.error, t)); } else { toast.success(t('settings.clearCacheSuccess')); } if (!shouldReloadAfterClear(result)) { // The retry stays actionable on this page; see shouldReloadAfterClear. setClearing(false); return; } // Reload without waiting. The stores are still live in memory, so the // longer this page stays up the more chances a `set()` has to persist // something after the clear. The seam refuses writes for the duration of // a clear, which covers writes issued while the deletes are in flight, // but not ones issued after they complete — hence keeping the window // short as well. window.location.reload(); } catch (error) { log.error('Failed to clear cache:', error); toast.error(clearCacheErrorMessage(error, t)); setClearing(false); } }, [isConfirmValid, t]); const clearCacheItems = t('settings.clearCacheConfirmItems').split('、').length > 1 ? t('settings.clearCacheConfirmItems').split('、') : t('settings.clearCacheConfirmItems').split(', '); return (
{/* Usage statistics dashboard */} {/* Danger Zone - Clear Cache */}
{/* Subtle diagonal stripe pattern for danger emphasis */}
{/* Header */}

{t('settings.dangerZone')}

{/* Content */}

{t('settings.clearCache')}

{t('settings.clearCacheDescription')}

{/* Clear Cache Confirmation Dialog */} { if (!clearing) { setShowClearDialog(open); if (!open) setConfirmInput(''); } }} > {t('settings.clearCacheConfirmTitle')}

{t('settings.clearCacheConfirmDescription')}

    {clearCacheItems.map((item, i) => (
  • {item.trim()}
  • ))}
setConfirmInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && isConfirmValid) { handleClearCache(); } }} autoFocus />
{t('common.cancel')}
); }