feat: update source files, Dockerfile, vite config and service worker

This commit is contained in:
2026-02-21 16:25:42 +00:00
parent 5e5aad54cb
commit 5c100a015d
11 changed files with 287 additions and 275 deletions
@@ -25,7 +25,7 @@ export const CCCabinetsPanel: React.FC = () => {
return createTheme({ palette: { mode, divider: 'var(--color-divider)' } });
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
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 API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined;
const apiFetch = async (url: string, init?: RequestInitLite) => {
@@ -75,7 +75,7 @@ export const CCCabinetsPanel: React.FC = () => {
<ThemeProvider theme={theme}>
<Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Toolbar>
<Button size="small" variant="outlined" startIcon={<AddIcon/>} onClick={() => { setNewName(''); setCreateOpen(true); }}>New Cabinet</Button>
<Button size="small" variant="outlined" startIcon={<AddIcon />} onClick={() => { setNewName(''); setCreateOpen(true); }}>New Cabinet</Button>
</Toolbar>
<Grid container spacing={1} sx={{ overflow: 'auto' }}>
{cabinets.map(c => (
@@ -1,17 +1,17 @@
import React, { useEffect, useMemo, useState, useCallback, useRef } from 'react';
import {
ThemeProvider,
createTheme,
useMediaQuery,
Button,
List,
ListItem,
ListItemText,
IconButton,
styled,
CircularProgress,
Divider,
Menu,
import {
ThemeProvider,
createTheme,
useMediaQuery,
Button,
List,
ListItem,
ListItemText,
IconButton,
styled,
CircularProgress,
Divider,
Menu,
MenuItem,
Box,
Typography,
@@ -42,8 +42,8 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import { useTLDraw } from '../../../../../contexts/TLDrawContext';
import { supabase } from '../../../../../supabaseClient';
import { useNavigate } from 'react-router-dom';
import {
calculateDirectoryStats,
import {
calculateDirectoryStats,
isDirectoryPickerSupported,
FileWithPath
} from '../../../../../utils/folderPicker';
@@ -57,15 +57,15 @@ const Container = styled('div')(() => ({
}));
type Cabinet = { id: string; name: string };
type FileRow = {
id: string;
name: string;
mime_type?: string;
is_directory?: boolean;
size_bytes?: number;
processing_status?: string;
relative_path?: string;
created_at?: string;
type FileRow = {
id: string;
name: string;
mime_type?: string;
is_directory?: boolean;
size_bytes?: number;
processing_status?: string;
relative_path?: string;
created_at?: string;
};
type Artefact = { id: string; type: string; rel_path: string; created_at: string };
@@ -101,7 +101,7 @@ export const CCFilesPanel: React.FC = () => {
const [loading, setLoading] = useState(false);
const [menuAnchor, setMenuAnchor] = useState<null | { el: HTMLElement; fileId: string }>(null);
const [artefacts, setArtefacts] = useState<Artefact[]>([]);
// Pagination and filtering state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(15); // Slightly more for main panel
@@ -109,13 +109,13 @@ export const CCFilesPanel: React.FC = () => {
const [sortBy, setSortBy] = useState('created_at');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const previousSearchTerm = useRef(searchTerm);
// Directory navigation state
const [currentDirectoryId, setCurrentDirectoryId] = useState<string | null>(null);
const [breadcrumbs, setBreadcrumbs] = useState<{ id: string | null; name: string }[]>([
{ id: null, name: 'Root' }
]);
// Directory upload state
const [selectedFiles, setSelectedFiles] = useState<FileWithPath[]>([]);
const [showDirectoryDialog, setShowDirectoryDialog] = useState(false);
@@ -126,7 +126,7 @@ export const CCFilesPanel: React.FC = () => {
totalSize: number;
formattedSize: string;
} | null>(null);
const navigate = useNavigate();
const theme = useMemo(() => {
@@ -139,7 +139,7 @@ export const CCFilesPanel: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>;
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 API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = {
@@ -168,7 +168,7 @@ export const CCFilesPanel: React.FC = () => {
const loadFiles = useCallback(async (cabinetId: string, page: number = currentPage) => {
if (!cabinetId) return;
setLoading(true);
try {
// Build query parameters for pagination, search, and sorting
@@ -180,19 +180,19 @@ export const CCFilesPanel: React.FC = () => {
sort_order: sortOrder,
include_directories: 'true'
});
// Add directory filtering
if (currentDirectoryId) {
params.append('parent_directory_id', currentDirectoryId);
}
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);
} catch (error) {
@@ -226,7 +226,7 @@ export const CCFilesPanel: React.FC = () => {
useEffect(() => {
if (selectedCabinet && searchTerm !== previousSearchTerm.current) {
previousSearchTerm.current = searchTerm;
const timeoutId = setTimeout(() => {
setCurrentPage(1); // Reset to first page when searching
loadFiles(selectedCabinet, 1);
@@ -239,10 +239,10 @@ export const CCFilesPanel: React.FC = () => {
// Directory navigation handlers
const navigateToFolder = useCallback((folder: FileRow) => {
if (!folder.is_directory) return;
setCurrentDirectoryId(folder.id);
setCurrentPage(1); // Reset to first page when entering folder
// Add to breadcrumbs
setBreadcrumbs(prev => [...prev, { id: folder.id, name: folder.name }]);
}, []);
@@ -250,7 +250,7 @@ export const CCFilesPanel: React.FC = () => {
const navigateToBreadcrumb = useCallback((targetBreadcrumb: { id: string | null; name: string }) => {
setCurrentDirectoryId(targetBreadcrumb.id);
setCurrentPage(1); // Reset to first page
// Trim breadcrumbs to the selected one
setBreadcrumbs(prev => {
const targetIndex = prev.findIndex(b => b.id === targetBreadcrumb.id && b.name === targetBreadcrumb.name);
@@ -264,7 +264,7 @@ export const CCFilesPanel: React.FC = () => {
// Directories come first
if (a.is_directory && !b.is_directory) return -1;
if (!a.is_directory && b.is_directory) return 1;
// Within the same type (both directories or both files), sort alphabetically by name
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
});
@@ -291,7 +291,7 @@ export const CCFilesPanel: React.FC = () => {
const handleDirectorySelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !selectedCabinet) return;
// Convert FileList to FileWithPath array with relative paths
const files: FileWithPath[] = [];
Array.from(e.target.files).forEach(file => {
@@ -299,11 +299,11 @@ export const CCFilesPanel: React.FC = () => {
(file as FileWithPath).relativePath = relativePath;
files.push(file as FileWithPath);
});
if (files.length > 0) {
prepareDirectoryUpload(files);
}
(e.target as HTMLInputElement).value = '';
};
@@ -331,30 +331,30 @@ export const CCFilesPanel: React.FC = () => {
if (!selectedCabinet || selectedFiles.length === 0) return;
setIsDirectoryUploading(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));
await apiFetch('/simple-upload/files/upload-directory', {
method: 'POST',
body: formData
await apiFetch('/simple-upload/files/upload-directory', {
method: 'POST',
body: formData
});
await loadFiles(selectedCabinet);
setShowDirectoryDialog(false);
setSelectedFiles([]);
setDirectoryStats(null);
@@ -387,10 +387,10 @@ export const CCFilesPanel: React.FC = () => {
const iconForMime = (mime?: string, isDirectory?: boolean) => {
if (isDirectory) return <FolderIcon />;
if (!mime) return <InsertDriveFileIcon/>;
if (mime.startsWith('image/')) return <ImageIcon/>;
if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon/>;
return <InsertDriveFileIcon/>;
if (!mime) return <InsertDriveFileIcon />;
if (mime.startsWith('image/')) return <ImageIcon />;
if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon />;
return <InsertDriveFileIcon />;
};
const formatFileSize = (bytes: number): string => {
@@ -436,10 +436,10 @@ export const CCFilesPanel: React.FC = () => {
))}
</Select>
</FormControl>
<Button
size="small"
variant="outlined"
<Button
size="small"
variant="outlined"
onClick={() => {
setCurrentPage(1);
setSearchTerm('');
@@ -486,7 +486,7 @@ export const CCFilesPanel: React.FC = () => {
<MenuItem value="size_bytes">Size</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 60 }}>
<InputLabel>Order</InputLabel>
<Select
@@ -498,7 +498,7 @@ export const CCFilesPanel: React.FC = () => {
<MenuItem value="desc"></MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 60 }}>
<InputLabel>Per page</InputLabel>
<Select
@@ -517,10 +517,10 @@ export const CCFilesPanel: React.FC = () => {
</Box>
{/* Breadcrumb Navigation */}
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
py: 1,
px: 1,
bgcolor: 'background.paper',
@@ -537,8 +537,8 @@ export const CCFilesPanel: React.FC = () => {
size="small"
variant="text"
onClick={() => navigateToBreadcrumb(breadcrumb)}
sx={{
minWidth: 'auto',
sx={{
minWidth: 'auto',
textTransform: 'none',
color: index === breadcrumbs.length - 1 ? 'primary.main' : 'text.secondary',
fontWeight: index === breadcrumbs.length - 1 ? 600 : 400
@@ -551,9 +551,9 @@ export const CCFilesPanel: React.FC = () => {
</Box>
{/* File List with Fixed Height */}
<Box sx={{
border: '1px solid var(--color-divider)',
borderRadius: '4px',
<Box sx={{
border: '1px solid var(--color-divider)',
borderRadius: '4px',
height: 300, // Fixed height for main panel
overflow: 'auto',
flex: 1,
@@ -565,7 +565,7 @@ export const CCFilesPanel: React.FC = () => {
}}>
{loading ? (
<Box sx={{ p: 2, textAlign: 'center' }}>
<CircularProgress size={20}/>
<CircularProgress size={20} />
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
Loading files...
</Typography>
@@ -593,17 +593,17 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={
<>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon/>
<MoreVertIcon />
</IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon/>
<DeleteIcon />
</IconButton>
</>
}
>
{iconForMime(f.mime_type, f.is_directory)}
<ListItemText
sx={{ ml: 1 }}
<ListItemText
sx={{ ml: 1 }}
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ wordBreak: 'break-all' }}>
@@ -611,9 +611,9 @@ export const CCFilesPanel: React.FC = () => {
</Typography>
{f.is_directory && <Chip label="Dir" size="small" />}
{f.processing_status && f.processing_status !== 'uploaded' && (
<Chip
label={f.processing_status}
size="small"
<Chip
label={f.processing_status}
size="small"
color={getStatusColor(f.processing_status)}
/>
)}
@@ -632,17 +632,17 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={
<>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon/>
<MoreVertIcon />
</IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon/>
<DeleteIcon />
</IconButton>
</>
}
>
{iconForMime(f.mime_type, f.is_directory)}
<ListItemText
sx={{ ml: 1 }}
<ListItemText
sx={{ ml: 1 }}
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ wordBreak: 'break-all' }}>
@@ -650,9 +650,9 @@ export const CCFilesPanel: React.FC = () => {
</Typography>
{f.is_directory && <Chip label="Dir" size="small" />}
{f.processing_status && f.processing_status !== 'uploaded' && (
<Chip
label={f.processing_status}
size="small"
<Chip
label={f.processing_status}
size="small"
color={getStatusColor(f.processing_status)}
/>
)}
@@ -683,7 +683,7 @@ export const CCFilesPanel: React.FC = () => {
{pagination && pagination.total_pages > 1 && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Stack spacing={1} alignItems="center">
<Pagination
<Pagination
count={pagination.total_pages}
page={pagination.page}
onChange={(event, value) => setCurrentPage(value)}
@@ -702,24 +702,24 @@ export const CCFilesPanel: React.FC = () => {
{/* Upload Controls */}
<Box sx={{ mt: 2, display: 'flex', gap: 1, flexDirection: 'column' }}>
{/* File Inputs */}
<input
id="cc-file-input"
type="file"
style={{ display: 'none' }}
<input
id="cc-file-input"
type="file"
style={{ display: 'none' }}
onChange={handleUpload}
disabled={!selectedCabinet}
/>
<input
id="cc-directory-input"
type="file"
<input
id="cc-directory-input"
type="file"
style={{ display: 'none' }}
{...({ webkitdirectory: '' } as React.InputHTMLAttributes<HTMLInputElement>)}
multiple
onChange={handleDirectorySelect}
disabled={!selectedCabinet}
/>
{/* Upload Buttons */}
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
@@ -731,7 +731,7 @@ export const CCFilesPanel: React.FC = () => {
>
Upload File
</Button>
<Button
variant="outlined"
startIcon={<FolderOpenIcon />}
@@ -742,13 +742,13 @@ export const CCFilesPanel: React.FC = () => {
Upload Folder
</Button>
</Box>
{!selectedCabinet && (
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', mt: 0.5 }}>
Select a cabinet first to enable uploads
</Typography>
)}
{selectedCabinet && !isDirectoryPickerSupported() && (
<Typography variant="caption" color="warning.main" sx={{ textAlign: 'center', mt: 0.5 }}>
Folder uploads may have limited support in this browser
@@ -769,11 +769,11 @@ export const CCFilesPanel: React.FC = () => {
{artefacts.length > 0 && (
<>
<Divider/>
<List dense sx={{
border: '1px solid var(--color-divider)',
borderRadius: '4px',
overflow: 'auto',
<Divider />
<List dense sx={{
border: '1px solid var(--color-divider)',
borderRadius: '4px',
overflow: 'auto',
maxHeight: 160,
// Hide scrollbar while keeping scroll functionality
scrollbarWidth: 'none', // Firefox
@@ -789,12 +789,12 @@ export const CCFilesPanel: React.FC = () => {
</List>
</>
)}
{/* Directory Upload Dialog */}
<Dialog
open={showDirectoryDialog}
onClose={() => !isDirectoryUploading && setShowDirectoryDialog(false)}
maxWidth="md"
<Dialog
open={showDirectoryDialog}
onClose={() => !isDirectoryUploading && setShowDirectoryDialog(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
@@ -804,21 +804,21 @@ export const CCFilesPanel: React.FC = () => {
{isDirectoryUploading && <LinearProgress sx={{ flexGrow: 1, ml: 2 }} />}
</Box>
</DialogTitle>
<DialogContent>
{directoryStats && (
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br/>
<strong>{directoryStats.directoryCount} folders</strong><br />
Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography>
</Alert>
)}
<Paper variant="outlined" sx={{
p: 2,
maxHeight: 200,
<Paper variant="outlined" sx={{
p: 2,
maxHeight: 200,
overflow: 'auto',
// Hide scrollbar while keeping scroll functionality
scrollbarWidth: 'none', // Firefox
@@ -841,14 +841,14 @@ export const CCFilesPanel: React.FC = () => {
))}
</Paper>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDirectoryDialog(false)} disabled={isDirectoryUploading}>
Cancel
</Button>
<Button
onClick={startDirectoryUpload}
variant="contained"
<Button
onClick={startDirectoryUpload}
variant="contained"
disabled={isDirectoryUploading || selectedFiles.length === 0}
>
{isDirectoryUploading ? 'Uploading...' : 'Upload Directory'}