'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Trash2, Download, Loader2, FolderOpen, Folder, FileText, Upload, FolderPlus, ChevronRight, Home, RefreshCw, FileImage, FileCode, FileArchive, FileSpreadsheet, } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { browseUserFiles, getUserFile, getUserFileDownloadUrl, uploadUserFile, deleteUserFile, createUserFileDir, getAccessToken, } from '@/lib/api'; import type { UserFileContent, UserFileItem } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { type AppLocale, pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; const LOAD_RETRY_DELAYS_MS = [0, 600, 1200]; function sleep(ms: number): Promise { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } export default function FilesPage() { const { locale } = useAppI18n(); const [items, setItems] = useState([]); const [currentPath, setCurrentPath] = useState(''); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [showMkdir, setShowMkdir] = useState(false); const [newDirName, setNewDirName] = useState(''); const [selectedFile, setSelectedFile] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(null); const fileInputRef = useRef(null); const mkdirInputRef = useRef(null); const load = useCallback(async (path: string = currentPath) => { let lastError: unknown = null; try { setLoading(true); setLoadError(null); for (const delay of LOAD_RETRY_DELAYS_MS) { if (delay > 0) { await sleep(delay); } try { const data = await browseUserFiles(path); setItems(data.items); setCurrentPath(data.path); setSelectedFile(null); setPreviewError(null); return; } catch (err) { lastError = err; } } const message = lastError instanceof Error ? lastError.message : pickAppText(locale, '加载文件失败', 'Failed to load files'); setLoadError(message); setItems([]); } finally { setLoading(false); } }, [currentPath, locale]); useEffect(() => { load(''); }, []); // eslint-disable-line react-hooks/exhaustive-deps const navigateTo = (path: string) => { load(path); }; const openFile = async (item: UserFileItem) => { if (item.type !== 'file') return; setPreviewLoading(true); setPreviewError(null); try { setSelectedFile(await getUserFile(item.path)); } catch (err: any) { setPreviewError(err.message || pickAppText(locale, '加载文件失败', 'Failed to load file')); setSelectedFile(null); } finally { setPreviewLoading(false); } }; const handleDelete = async (item: UserFileItem) => { const label = item.type === 'directory' ? pickAppText(locale, '文件夹', 'folder') : pickAppText(locale, '文件', 'file'); if (!confirm(pickAppText( locale, `确定删除${label} "${item.name}"?${item.type === 'directory' ? '(包含所有子文件)' : ''}`, `Delete ${label} "${item.name}"?${item.type === 'directory' ? ' (including all nested files)' : ''}` ))) { return; } try { await deleteUserFile(item.path); setItems((prev) => prev.filter((i) => i.path !== item.path)); if (selectedFile?.path === item.path) { setSelectedFile(null); } } catch { // ignore } }; const handleDownload = async (item: UserFileItem) => { const url = getUserFileDownloadUrl(item.path); const token = getAccessToken(); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; try { const res = await fetch(url, { headers }); const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = item.name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); } catch { // ignore } }; const handleUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; setUploading(true); setUploadProgress(0); try { for (let i = 0; i < files.length; i++) { await uploadUserFile(files[i], currentPath || 'uploads', (pct) => { setUploadProgress(Math.round((i / files.length) * 100 + pct / files.length)); }); } await load(); } catch { // ignore } finally { setUploading(false); setUploadProgress(0); if (fileInputRef.current) fileInputRef.current.value = ''; } }; const handleCreateDir = async () => { const name = newDirName.trim(); if (!name) return; try { const dirPath = currentPath ? `${currentPath}/${name}` : name; await createUserFileDir(dirPath); setShowMkdir(false); setNewDirName(''); await load(); } catch { // ignore } }; // Build breadcrumbs const breadcrumbs = currentPath ? currentPath.split('/') : []; const formatSize = (bytes: number | null) => { if (bytes === null || bytes === undefined) return ''; if (bytes > 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; if (bytes > 1024) return `${(bytes / 1024).toFixed(0)} KB`; return `${bytes} B`; }; const formatDate = (iso: string | null | undefined) => { if (!iso) return ''; try { return new Date(iso).toLocaleString(locale, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } catch { return ''; } }; return (
{/* Header */}

{pickAppText(locale, '文件管理', 'Files')}

{/* Breadcrumbs */}
{breadcrumbs.map((segment, idx) => { const path = breadcrumbs.slice(0, idx + 1).join('/'); const isLast = idx === breadcrumbs.length - 1; return ( ); })}
{/* New directory input */} {showMkdir && (
setNewDirName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateDir(); if (e.key === 'Escape') { setShowMkdir(false); setNewDirName(''); } }} placeholder={pickAppText(locale, '文件夹名称', 'Folder name')} className="flex-1 px-3 py-1.5 text-sm border border-border rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring" autoFocus />
)}
{/* File list */}
{loading && items.length === 0 ? (
) : loadError ? (

{pickAppText(locale, '加载失败', 'Failed to load')}

{loadError}

) : items.length === 0 ? (

{pickAppText(locale, '空文件夹', 'Empty folder')}

{pickAppText(locale, '点击上方"上传"或"新建文件夹"按钮开始使用', 'Use "Upload" or "New folder" above to get started')}

) : (
{items.map((item) => ( ))}
)}
); } function FilePreviewPanel({ file, loading, error, formatSize, formatDate, downloadUrl, locale, }: { file: UserFileContent | null; loading: boolean; error: string | null; formatSize: (bytes: number | null) => string; formatDate: (iso: string | null | undefined) => string; downloadUrl: string | null; locale: AppLocale; }) { return (
{loading ? (
) : error ? (
{error}
) : !file ? (

{pickAppText(locale, '点击左侧文件查看内容', 'Click a file to preview its contents')}

) : (

{file.name}

{formatSize(file.size)} · {formatDate(file.modified)} · {file.content_type} {file.is_truncated ? ` · ${pickAppText(locale, '仅预览前 1MB', 'Showing first 1MB')}` : ''}

{downloadUrl && ( )}
{isImage(file) && downloadUrl ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {file.name}
) : file.is_binary ? (

{pickAppText(locale, '该文件不能直接预览', 'This file cannot be previewed')}

) : isMarkdown(file) ? (
{file.content || ''}
) : (
              {file.content || ''}
            
)}
)}
); } function FileIcon({ name, contentType }: { name: string; contentType?: string }) { const ext = name.split('.').pop()?.toLowerCase() || ''; const ct = contentType || ''; if (ct.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext)) { return ; } if ( ['js', 'ts', 'tsx', 'jsx', 'py', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'css', 'html', 'json', 'yaml', 'yml', 'toml', 'md', 'sh'].includes(ext) ) { return ; } if (['zip', 'tar', 'gz', 'bz2', 'rar', '7z', 'xz'].includes(ext)) { return ; } if (['csv', 'xls', 'xlsx', 'tsv'].includes(ext)) { return ; } return ; } function isImage(file: UserFileContent): boolean { return file.content_type.startsWith('image/'); } function isMarkdown(file: UserFileContent): boolean { return file.path.toLowerCase().endsWith('.md') || file.content_type.includes('markdown'); }