import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Box, Typography, Button, Card, CardContent, CardHeader, Grid, Alert, Chip, LinearProgress, List, ListItem, ListItemText, ListItemIcon, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Divider, Select, MenuItem, FormControl, InputLabel, Paper, TextField, Pagination, Stack } from '@mui/material'; import { CloudUpload as UploadIcon, Upload, Folder as FolderIcon, FolderOpen as FolderOpenIcon, Description as FileIcon, Delete as DeleteIcon, Refresh as RefreshIcon, PlayArrow as ProcessIcon, CheckCircle as SuccessIcon, Error as ErrorIcon, Info as InfoIcon, InsertDriveFile, } from '@mui/icons-material'; const FolderOpen = FolderOpenIcon; const Refresh = RefreshIcon; const Info = InfoIcon; import { supabase } from '../../supabaseClient'; import { pickDirectory, processDirectoryFiles, calculateDirectoryStats, formatFileSize, isDirectoryPickerSupported, FileWithPath } from '../../utils/folderPicker'; interface Cabinet { id: string; name: string; } interface FileRecord { id: string; name: string; mime_type?: string; is_directory?: boolean; size_bytes?: number; processing_status?: string; relative_path?: string; created_at?: string; } interface PaginationInfo { page: number; per_page: number; total_count: number; total_pages: number; has_next: boolean; has_prev: boolean; offset: number; } interface FileListResponse { files: FileRecord[]; pagination: PaginationInfo; filters: { search?: string; sort_by: string; sort_order: string; include_directories: boolean; parent_directory_id?: string; }; } interface UploadProgress { path: string; size: number; status: 'queued' | 'uploading' | 'done' | 'error'; progress: number; error?: string; } const SimpleUploadTest: React.FC = () => { // State management const [cabinets, setCabinets] = useState([]); const [selectedCabinet, setSelectedCabinet] = useState(''); const [files, setFiles] = useState([]); const [pagination, setPagination] = useState(null); const [loading, setLoading] = useState(false); // Pagination and filtering state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState('created_at'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [uploadProgress, setUploadProgress] = useState([]); const [showUploadDialog, setShowUploadDialog] = useState(false); const [isUploading, setIsUploading] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [directoryStats, setDirectoryStats] = useState(null); const [message, setMessage] = useState<{ type: 'success' | 'error' | 'info', text: string } | null>(null); const [uploadType, setUploadType] = useState<'old' | 'new'>('new'); // Refs const fileInputRef = useRef(null); const dirInputRef = useRef(null); const API_BASE = import.meta.env.VITE_API_BASE || '/api'; const apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record }) => { const session = await supabase.auth.getSession(); const token = session?.data?.session?.access_token; if (!token) { throw new Error('No authentication token available'); } const headers = { 'Authorization': `Bearer ${token}`, ...((init?.headers as Record) || {}) }; const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; const res = await fetch(fullUrl, { ...init, headers }); if (!res.ok) { const errorText = await res.text(); throw new Error(`HTTP ${res.status}: ${errorText}`); } return res.json(); }, [API_BASE]); // Load cabinets and files const loadCabinets = useCallback(async () => { setLoading(true); try { const data = await apiFetch('/database/cabinets'); const all = [...(data.owned || []), ...(data.shared || [])]; setCabinets(all); if (all.length && !selectedCabinet) { setSelectedCabinet(all[0].id); } setMessage({ type: 'success', text: `Loaded ${all.length} cabinets` }); } catch (error: unknown) { console.error('Failed to load cabinets:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Failed to load cabinets: ${errorMessage}` }); } finally { setLoading(false); } }, [selectedCabinet, apiFetch]); const loadFiles = useCallback(async (cabinetId: string, page: number = currentPage) => { if (!cabinetId) return; setLoading(true); try { // Build query parameters for pagination, search, and sorting const params = new URLSearchParams({ cabinet_id: cabinetId, page: page.toString(), per_page: itemsPerPage.toString(), sort_by: sortBy, sort_order: sortOrder, include_directories: 'true' }); if (searchTerm) { params.append('search', searchTerm); } // Use the new simple upload endpoint for listing files with pagination const data: FileListResponse = await apiFetch(`/simple-upload/files?${params.toString()}`); setFiles(data.files || []); setPagination(data.pagination); setMessage({ type: 'success', text: `Loaded ${data.files?.length || 0} files (${data.pagination.total_count} total)` }); } catch (error: unknown) { console.error('Failed to load files:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Failed to load files: ${errorMessage}` }); } finally { setLoading(false); } }, [currentPage, itemsPerPage, sortBy, sortOrder, searchTerm, apiFetch]); useEffect(() => { loadCabinets(); }, [loadCabinets]); useEffect(() => { if (selectedCabinet) { setCurrentPage(1); // Reset to first page when cabinet changes loadFiles(selectedCabinet, 1); } }, [selectedCabinet, loadFiles]); // Reload files when pagination/filtering parameters change useEffect(() => { if (selectedCabinet) { loadFiles(selectedCabinet, currentPage); } }, [selectedCabinet, loadFiles, currentPage]); // Search with debouncing useEffect(() => { if (selectedCabinet) { const timeoutId = setTimeout(() => { setCurrentPage(1); // Reset to first page when searching loadFiles(selectedCabinet, 1); }, 500); // 500ms debounce return () => clearTimeout(timeoutId); } }, [searchTerm, selectedCabinet, loadFiles]); // Single file upload const handleSingleUpload = async (e: React.ChangeEvent) => { if (!e.target.files || !selectedCabinet) return; const file = e.target.files[0]; const formData = new FormData(); formData.append('cabinet_id', selectedCabinet); formData.append('path', file.name); formData.append('scope', 'teacher'); formData.append('file', file); try { setLoading(true); // Choose endpoint based on upload type const endpoint = uploadType === 'new' ? '/simple-upload/files/upload' : '/database/files/upload'; const result = await apiFetch(endpoint, { method: 'POST', body: formData }); console.log('Upload result:', result); setMessage({ type: 'success', text: `File uploaded successfully using ${uploadType === 'new' ? 'NEW' : 'OLD'} endpoint: ${file.name}` }); await loadFiles(selectedCabinet); e.target.value = ''; } catch (error: unknown) { console.error('Upload failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Upload failed: ${errorMessage}` }); } finally { setLoading(false); } }; // Directory upload handling const handleDirectoryPicker = async () => { try { const files = await pickDirectory(); prepareDirectoryUpload(files); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (errorMessage === 'fallback-input') { dirInputRef.current?.click(); } else if (errorMessage === 'user-cancelled') { setMessage({ type: 'info', text: 'Directory selection cancelled' }); } else { console.error('Directory picker error:', error); setMessage({ type: 'error', text: 'Failed to pick directory. Trying fallback method...' }); dirInputRef.current?.click(); } } }; const handleFallbackDirectorySelect = (e: React.ChangeEvent) => { if (!e.target.files) return; const files = processDirectoryFiles(e.target.files); prepareDirectoryUpload(files); e.target.value = ''; }; const prepareDirectoryUpload = (files: FileWithPath[]) => { if (files.length === 0) { setMessage({ type: 'error', text: 'No files selected' }); return; } setSelectedFiles(files); setDirectoryStats(calculateDirectoryStats(files)); const progress: UploadProgress[] = files.map(file => ({ path: file.relativePath, size: file.size, status: 'queued', progress: 0 })); setUploadProgress(progress); setShowUploadDialog(true); }; const startDirectoryUpload = async () => { if (!selectedCabinet || selectedFiles.length === 0) return; setIsUploading(true); try { const firstFilePath = selectedFiles[0].relativePath; const directoryName = firstFilePath.split('/')[0] || 'uploaded-folder'; const formData = new FormData(); formData.append('cabinet_id', selectedCabinet); formData.append('scope', 'teacher'); formData.append('directory_name', directoryName); selectedFiles.forEach(file => { formData.append('files', file); }); const relativePaths = selectedFiles.map(f => f.relativePath); formData.append('file_paths', JSON.stringify(relativePaths)); const result = await apiFetch('/simple-upload/files/upload-directory', { method: 'POST', body: formData }); console.log('Directory upload result:', result); setUploadProgress(prev => prev.map(item => ({ ...item, status: 'done', progress: 100 }))); setMessage({ type: 'success', text: `Directory uploaded successfully: ${directoryName} (${selectedFiles.length} files)` }); await loadFiles(selectedCabinet); setTimeout(() => { setShowUploadDialog(false); setIsUploading(false); setSelectedFiles([]); setUploadProgress([]); }, 2000); } catch (error: unknown) { console.error('Directory upload failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Directory upload failed: ${errorMessage}` }); setUploadProgress(prev => prev.map(item => ({ ...item, status: 'error', error: String(error) }))); setIsUploading(false); } }; // Delete file const handleDelete = async (fileId: string) => { try { await apiFetch(`/simple-upload/files/${fileId}`, { method: 'DELETE' }); setMessage({ type: 'success', text: 'File deleted successfully' }); await loadFiles(selectedCabinet); } catch (error: unknown) { console.error('Delete failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Delete failed: ${errorMessage}` }); } }; // Manual processing trigger const handleManualProcessing = async (fileId: string) => { try { const formData = new FormData(); formData.append('processing_type', 'basic'); const result = await apiFetch(`/simple-upload/files/${fileId}/process-manual`, { method: 'POST', body: formData }); console.log('Manual processing result:', result); setMessage({ type: 'info', text: 'Manual processing triggered (not yet implemented)' }); } catch (error: unknown) { console.error('Manual processing failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setMessage({ type: 'error', text: `Manual processing failed: ${errorMessage}` }); } }; const getStatusColor = (status?: string) => { switch (status) { case 'uploaded': return 'primary'; case 'processing': return 'warning'; case 'completed': return 'success'; case 'failed': return 'error'; default: return 'default'; } }; return ( 🧪 Simple Upload Test Page This page tests the NEW simple upload system (no auto-processing) vs the OLD system (with auto-processing). Use this to verify that files upload without triggering Docling bundles, Tika runs, etc. {message && ( setMessage(null)}> {message.text} )} {/* Upload Controls */} } /> Cabinet Upload Type {isDirectoryPickerSupported() ? ( ✅ Modern directory picker supported (Chromium browser) ) : ( ℹ️ Using fallback directory picker (webkitdirectory) )} {/* System Info */} } /> Current Endpoints: • NEW: /simple-upload/files/upload (no auto-processing) • OLD: /database/files/upload (auto-processing disabled for testing) Storage Buckets: Both systems now use: cc.users (teacher scope) Files will be stored in the same bucket for consistency Selected Cabinet: {selectedCabinet || 'None selected'} Upload Mode: Files in Cabinet: {pagination ? `${pagination.total_count} total (${files.length} on page ${pagination.page})` : files.length} Pagination: {itemsPerPage} per page • Sort by {sortBy} ({sortOrder}) {searchTerm && ( Searching: "{searchTerm}" )} {/* File List */} Files {pagination && `(${pagination.total_count} total)`} {pagination && ( Page {pagination.page} of {pagination.total_pages} )} } /> {/* Search and Filter Controls */} setSearchTerm(e.target.value)} sx={{ minWidth: 200 }} /> Sort by Order Per page {/* File List with Fixed Height */} {loading ? ( Loading files... ) : files.length === 0 ? ( {searchTerm ? 'No files found matching your search.' : 'No files found. Upload some files to test!'} ) : ( {files.map((file, index) => ( handleManualProcessing(file.id)} title="Trigger manual processing" > handleDelete(file.id)} title="Delete file" color="error" > } > {file.is_directory ? : } {file.name} {file.is_directory && } } secondary={ {file.size_bytes ? formatFileSize(file.size_bytes) : 'Unknown size'} • {file.mime_type || 'Unknown type'} {file.relative_path && ( Path: {file.relative_path} )} } /> {index < files.length - 1 && } ))} )} {/* Pagination Controls */} {pagination && pagination.total_pages > 1 && ( setCurrentPage(value)} color="primary" showFirstButton showLastButton /> Showing {pagination.offset + 1}-{Math.min(pagination.offset + pagination.per_page, pagination.total_count)} of {pagination.total_count} files )} {/* Directory Upload Dialog */} !isUploading && setShowUploadDialog(false)} maxWidth="md" fullWidth> Directory Upload Progress {isUploading && } {directoryStats && ( {directoryStats.fileCount} files in{' '} {directoryStats.directoryCount} folders
Total size: {directoryStats.formattedSize}
)} {uploadProgress.map((item, i) => ( {item.path} {formatFileSize(item.size)} : item.status === 'error' ? : undefined } /> ))}
); }; export default SimpleUploadTest;