import React, { useEffect, useMemo, useState, useRef } from 'react'; import { ThemeProvider, createTheme, useMediaQuery, Button, List, ListItem, ListItemText, IconButton, styled, CircularProgress, Divider, Menu, MenuItem, Box, Typography, LinearProgress, Dialog, DialogTitle, DialogContent, DialogActions, Chip, Tooltip, Alert } from '@mui/material'; import UploadIcon from '@mui/icons-material/Upload'; import FolderIcon from '@mui/icons-material/Folder'; import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import DeleteIcon from '@mui/icons-material/Delete'; import RefreshIcon from '@mui/icons-material/Refresh'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import ImageIcon from '@mui/icons-material/Image'; import DescriptionIcon from '@mui/icons-material/Description'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import { useTLDraw } from '../../../../../contexts/TLDrawContext'; import { useAuth } from '../../../../../contexts/AuthContext'; import { useNavigate } from 'react-router-dom'; import { pickDirectory, processDirectoryFiles, calculateDirectoryStats, createDirectoryTree, formatFileSize, isDirectoryPickerSupported, FileWithPath } from '../../../../folderPicker'; import pLimit from 'p-limit'; const Container = styled('div')(() => ({ padding: '8px', display: 'flex', flexDirection: 'column', gap: '8px', height: '100%' })); const Row = styled('div')(() => ({ display: 'flex', gap: '8px', alignItems: 'center' })); type Cabinet = { id: string; name: string }; type FileRow = { id: string; name: string; mime_type?: string; is_directory?: boolean; size_bytes?: number }; type Artefact = { id: string; type: string; rel_path: string; created_at: string }; interface UploadProgress { path: string; size: number; status: 'queued' | 'uploading' | 'done' | 'error'; progress: number; error?: string; } export const CCFilesPanelEnhanced: React.FC = () => { const { tldrawPreferences } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' } }; const { user: authUser, accessToken } = useAuth(); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const [cabinets, setCabinets] = useState([]); const [selectedCabinet, setSelectedCabinet] = useState(''); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [menuAnchor, setMenuAnchor] = useState(null); const [artefacts, setArtefacts] = useState([]); // Directory upload states const [uploadProgress, setUploadProgress] = useState([]); const [showUploadDialog, setShowUploadDialog] = useState(false); const [isUploading, setIsUploading] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [directoryStats, setDirectoryStats] = useState(null); const navigate = useNavigate(); const fileInputRef = useRef(null); const dirInputRef = useRef(null); const theme = useMemo(() => { const mode = (tldrawPreferences?.colorScheme === 'system') ? (prefersDarkMode ? 'dark' : 'light') : (tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light'); return createTheme({ palette: { mode, divider: 'var(--color-divider)' } }); }, [tldrawPreferences?.colorScheme, prefersDarkMode]); type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record } | undefined; type HeadersInitLike = Record; const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const apiFetch = async (url: string, init?: RequestInitLike) => { const headers: HeadersInitLike = { 'Authorization': `Bearer ${accessToken || ''}`, ...(init?.headers || {}) }; const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; const res = await fetch(fullUrl, { ...(init || {}), headers }); if (!res.ok) throw new Error(await res.text()); return res.json(); }; const loadCabinets = 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); } finally { setLoading(false); } }; const loadFiles = async (cabinetId: string) => { setLoading(true); try { const data = await apiFetch(`/simple-upload/files?cabinet_id=${encodeURIComponent(cabinetId)}`); setFiles(data.files || []); } finally { setLoading(false); } }; useEffect(() => { if (authUser?.id) { loadCabinets(); } }, [authUser?.id]); useEffect(() => { if (selectedCabinet) loadFiles(selectedCabinet); }, [selectedCabinet]); const handleSingleUpload = async (e: React.ChangeEvent) => { if (!e.target.files || !selectedCabinet) return; const file = e.target.files[0]; const form = new FormData(); form.append('cabinet_id', selectedCabinet); form.append('path', file.name); form.append('scope', 'teacher'); form.append('file', file); try { await apiFetch('/simple-upload/files/upload', { method: 'POST', body: form }); await loadFiles(selectedCabinet); (e.target as HTMLInputElement).value = ''; } catch (error) { console.error('Upload failed:', error); alert(`Upload failed: ${error}`); } }; const handleDirectoryPicker = async () => { try { const files = await pickDirectory(); prepareDirectoryUpload(files); } catch (error: any) { if (error.message === 'fallback-input') { // Use fallback input dirInputRef.current?.click(); } else if (error.message === 'user-cancelled') { // User cancelled, do nothing } else { console.error('Directory picker error:', error); alert('Failed to pick directory. Please try the 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 = ''; // Reset input }; const prepareDirectoryUpload = (files: FileWithPath[]) => { if (files.length === 0) { alert('No files selected'); return; } setSelectedFiles(files); setDirectoryStats(calculateDirectoryStats(files)); // Initialize upload progress 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 { // Get directory name from first file's path const firstFilePath = selectedFiles[0].relativePath; const directoryName = firstFilePath.split('/')[0] || 'uploaded-folder'; // Prepare form data const formData = new FormData(); formData.append('cabinet_id', selectedCabinet); formData.append('scope', 'teacher'); formData.append('directory_name', directoryName); // Add all files selectedFiles.forEach(file => { formData.append('files', file); }); // Add relative paths as JSON const relativePaths = selectedFiles.map(f => f.relativePath); formData.append('file_paths', JSON.stringify(relativePaths)); // Upload directory const result = await apiFetch('/simple-upload/files/upload-directory', { method: 'POST', body: formData }); console.log('Directory upload result:', result); // Update progress to completed setUploadProgress(prev => prev.map(item => ({ ...item, status: 'done', progress: 100 }))); // Refresh file list await loadFiles(selectedCabinet); // Close dialog after a short delay setTimeout(() => { setShowUploadDialog(false); setIsUploading(false); setSelectedFiles([]); setUploadProgress([]); }, 2000); } catch (error) { console.error('Directory upload failed:', error); alert(`Directory upload failed: ${error}`); // Mark all as error setUploadProgress(prev => prev.map(item => ({ ...item, status: 'error', error: String(error) }))); setIsUploading(false); } }; const handleDelete = async (fileId: string) => { try { await apiFetch(`/simple-upload/files/${fileId}`, { method: 'DELETE' }); await loadFiles(selectedCabinet); } catch (error) { console.error('Delete failed:', error); alert(`Delete failed: ${error}`); } }; const handleGenerateInitial = async (fileId: string) => { // This would trigger manual processing if we implement it later alert('Manual processing not yet implemented'); }; const openMenu = (el: HTMLElement, fileId: string) => setMenuAnchor({ el, fileId }); const closeMenu = () => setMenuAnchor(null); const goToAIContent = () => { if (!menuAnchor) return; const fileId = menuAnchor.fileId; closeMenu(); navigate(`/doc-intelligence/${encodeURIComponent(fileId)}`); }; const iconForMime = (mime?: string, isDirectory?: boolean) => { if (isDirectory) return ; if (!mime) return ; if (mime.startsWith('image/')) return ; if (mime === 'application/pdf' || mime.startsWith('application/')) return ; return ; }; const formatFileInfo = (file: FileRow) => { if (file.is_directory) { return `Directory • ${file.size_bytes ? formatFileSize(file.size_bytes) : 'Unknown size'}`; } return file.size_bytes ? formatFileSize(file.size_bytes) : 'Unknown size'; }; return ( {cabinets.map(c => ( setSelectedCabinet(c.id)} sx={{ cursor: 'pointer' }}> ))} {/* Single file upload */} {/* Directory upload */} {loading ? : ( {files.map(f => ( openMenu(e.currentTarget, f.id)} title="File actions"> handleDelete(f.id)} title="Delete file"> } > {iconForMime(f.mime_type, f.is_directory)} {f.is_directory && } ))} )} { if (menuAnchor) { handleGenerateInitial(menuAnchor.fileId); closeMenu(); } }}> Process manually Open AI content {/* Directory Upload Dialog */} !isUploading && setShowUploadDialog(false)} maxWidth="md" fullWidth> Directory Upload {isUploading && } {directoryStats && ( {directoryStats.fileCount} files in{' '} {directoryStats.directoryCount} folders
Total size: {directoryStats.formattedSize}
)} Upload Progress {uploadProgress.length > 0 && ( <> {uploadProgress.filter(p => p.status === 'done').length} / {uploadProgress.length} files completed p.status === 'done').length / uploadProgress.length) * 100} sx={{ mt: 1 }} /> {uploadProgress.map((item, i) => ( ))}
Path Size Status
{item.path} {formatFileSize(item.size)}
)}
); }; export default CCFilesPanelEnhanced;