/ src / views / settings / section-storage.tsx
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  }