section-storage.tsx
1 'use client' 2 3 import { useState, useEffect, useCallback } from 'react' 4 import { api } from '@/lib/app/api-client' 5 import { BottomSheet } from '@/components/shared/bottom-sheet' 6 import { ConfirmDialog } from '@/components/shared/confirm-dialog' 7 import { StorageBrowser } from './storage-browser' 8 import { formatBytes } from '@/lib/format-display' 9 import type { SettingsSectionProps } from './types' 10 11 interface UploadFile { 12 name: string 13 size: number 14 modified: number 15 category: string 16 url: string 17 } 18 19 interface UploadsResponse { 20 files: UploadFile[] 21 totalSize: number 22 count: number 23 } 24 25 26 27 export function StorageSection( 28 // eslint-disable-next-line @typescript-eslint/no-unused-vars 29 _props: SettingsSectionProps, 30 ) { 31 const [data, setData] = useState<UploadsResponse | null>(null) 32 const [loading, setLoading] = useState(true) 33 const [browserOpen, setBrowserOpen] = useState(false) 34 const [confirmAction, setConfirmAction] = useState<'clearOld' | 'clearAll' | null>(null) 35 const [deleting, setDeleting] = useState(false) 36 37 const fetchFiles = useCallback(async () => { 38 try { 39 setLoading(true) 40 const res = await api<UploadsResponse>('GET', '/uploads') 41 setData(res) 42 } catch { 43 // silent — section just shows empty 44 } finally { 45 setLoading(false) 46 } 47 }, []) 48 49 useEffect(() => { 50 fetchFiles() 51 // eslint-disable-next-line react-hooks/exhaustive-deps 52 }, []) 53 54 const handleBulkDelete = useCallback(async (filenames: string[]) => { 55 try { 56 await api('DELETE', '/uploads', { filenames }) 57 await fetchFiles() 58 } catch { 59 // silent 60 } 61 }, [fetchFiles]) 62 63 const handleConfirmAction = useCallback(async () => { 64 if (!confirmAction) return 65 setDeleting(true) 66 try { 67 if (confirmAction === 'clearOld') { 68 await api('DELETE', '/uploads', { olderThanDays: 30 }) 69 } else { 70 await api('DELETE', '/uploads', { all: true }) 71 } 72 await fetchFiles() 73 } catch { 74 // silent 75 } finally { 76 setDeleting(false) 77 setConfirmAction(null) 78 } 79 }, [confirmAction, fetchFiles]) 80 81 // Breakdown by category 82 const breakdown = data?.files.reduce<Record<string, { count: number; size: number }>>((acc, f) => { 83 if (!acc[f.category]) acc[f.category] = { count: 0, size: 0 } 84 acc[f.category].count += 1 85 acc[f.category].size += f.size 86 return acc 87 }, {}) ?? {} 88 89 const CATEGORY_LABELS: Record<string, string> = { 90 image: 'Images', 91 video: 'Videos', 92 audio: 'Audio', 93 document: 'Documents', 94 archive: 'Archives', 95 other: 'Other', 96 } 97 98 return ( 99 <div className="mb-10"> 100 <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2"> 101 Storage 102 </h3> 103 <p className="text-[12px] text-text-3 mb-5"> 104 Uploaded files from agent tools (screenshots, images, documents). Manage disk usage. 105 </p> 106 107 <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]"> 108 {/* Summary */} 109 {loading ? ( 110 <div className="text-[13px] text-text-3/60 animate-pulse">Loading storage info...</div> 111 ) : ( 112 <> 113 <div className="flex items-baseline gap-3 mb-4"> 114 <span className="font-display text-[28px] font-700 tracking-[-0.02em] text-text"> 115 {formatBytes(data?.totalSize ?? 0)} 116 </span> 117 <span className="text-[13px] text-text-3"> 118 {data?.count ?? 0} file{data?.count !== 1 ? 's' : ''} 119 </span> 120 </div> 121 122 {/* Category breakdown */} 123 {Object.keys(breakdown).length > 0 && ( 124 <div className="flex flex-wrap gap-x-4 gap-y-1 mb-5"> 125 {Object.entries(breakdown).map(([cat, info]) => ( 126 <span key={cat} className="text-[11px] text-text-3/70"> 127 {CATEGORY_LABELS[cat] || cat}: {info.count} ({formatBytes(info.size)}) 128 </span> 129 ))} 130 </div> 131 )} 132 133 {/* Actions */} 134 <div className="flex flex-wrap gap-2"> 135 <button 136 onClick={() => setBrowserOpen(true)} 137 disabled={!data?.count} 138 className="px-4 py-2.5 rounded-[12px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer 139 hover:brightness-110 active:scale-[0.97] transition-all border border-accent-bright/20 140 disabled:opacity-40 disabled:cursor-not-allowed" 141 style={{ fontFamily: 'inherit' }} 142 > 143 Manage Files 144 </button> 145 <button 146 onClick={() => setConfirmAction('clearOld')} 147 disabled={!data?.count} 148 className="px-4 py-2.5 rounded-[12px] bg-white/[0.04] text-text-2 text-[12px] font-600 cursor-pointer 149 hover:bg-white/[0.06] active:scale-[0.97] transition-all border border-white/[0.06] 150 disabled:opacity-40 disabled:cursor-not-allowed" 151 style={{ fontFamily: 'inherit' }} 152 > 153 Clear Old Files 154 </button> 155 <button 156 onClick={() => setConfirmAction('clearAll')} 157 disabled={!data?.count} 158 className="px-4 py-2.5 rounded-[12px] bg-danger/10 text-danger text-[12px] font-600 cursor-pointer 159 hover:bg-danger/20 active:scale-[0.97] transition-all border border-danger/20 160 disabled:opacity-40 disabled:cursor-not-allowed" 161 style={{ fontFamily: 'inherit' }} 162 > 163 Clear All 164 </button> 165 </div> 166 </> 167 )} 168 </div> 169 170 {/* File browser sheet */} 171 <BottomSheet open={browserOpen} onClose={() => setBrowserOpen(false)} wide> 172 {data && ( 173 <StorageBrowser 174 files={data.files} 175 onDelete={handleBulkDelete} 176 /> 177 )} 178 </BottomSheet> 179 180 {/* Confirm dialogs */} 181 <ConfirmDialog 182 open={confirmAction === 'clearOld'} 183 title="Clear Old Files" 184 message="Delete all uploaded files older than 30 days? This cannot be undone." 185 confirmLabel={deleting ? 'Deleting...' : 'Delete Old Files'} 186 danger 187 onConfirm={handleConfirmAction} 188 onCancel={() => setConfirmAction(null)} 189 /> 190 <ConfirmDialog 191 open={confirmAction === 'clearAll'} 192 title="Clear All Files" 193 message="Delete ALL uploaded files? This will free up all storage but cannot be undone." 194 confirmLabel={deleting ? 'Deleting...' : 'Delete All'} 195 danger 196 onConfirm={handleConfirmAction} 197 onCancel={() => setConfirmAction(null)} 198 /> 199 </div> 200 ) 201 }