feat: update source files, Dockerfile, vite config and service worker
This commit is contained in:
parent
5e5aad54cb
commit
5c100a015d
21
Dockerfile
21
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM node:20 as builder
|
||||
FROM node:20 AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
# TODO: Remove this or review embedded variables
|
||||
@ -20,19 +20,24 @@ RUN echo 'server { \
|
||||
root /usr/share/nginx/html; \
|
||||
index index.html; \
|
||||
location / { \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
expires 30d; \
|
||||
add_header Cache-Control "public, no-transform"; \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
expires 30d; \
|
||||
add_header Cache-Control "public, no-transform"; \
|
||||
} \
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \
|
||||
expires 30d; \
|
||||
add_header Cache-Control "public, no-transform"; \
|
||||
expires 30d; \
|
||||
add_header Cache-Control "public, no-transform"; \
|
||||
} \
|
||||
location /searxng-api/ { \
|
||||
proxy_pass https://search.kevlarai.com/; \
|
||||
proxy_ssl_server_name on; \
|
||||
proxy_set_header Host search.kevlarai.com; \
|
||||
} \
|
||||
location ~ /\. { \
|
||||
deny all; \
|
||||
deny all; \
|
||||
} \
|
||||
error_page 404 /index.html; \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Set up permissions
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html \
|
||||
|
||||
11
package.json
11
package.json
@ -57,14 +57,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@storybook/addon-actions": "^8.6.12",
|
||||
"@storybook/addon-essentials": "^8.6.12",
|
||||
"@storybook/addon-interactions": "^8.6.12",
|
||||
"@storybook/addon-links": "^8.6.12",
|
||||
"@storybook/addon-onboarding": "^8.6.12",
|
||||
"@storybook/react": "^8.6.12",
|
||||
"@storybook/react-vite": "^8.6.12",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/react": "^18.2.66",
|
||||
@ -90,10 +82,9 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"react-refresh": "^0.14.2",
|
||||
"storybook": "^8.6.12",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,9 @@ import { DatabaseNameService } from '../services/graph/databaseNameService';
|
||||
import { provisionUser } from '../services/provisioningService';
|
||||
import { storageService, StorageKeys } from '../services/auth/localStorageService';
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
export interface UserContextType {
|
||||
user: CCUser | null;
|
||||
loading: boolean;
|
||||
@ -29,9 +32,9 @@ export const UserContext = createContext<UserContextType>({
|
||||
preferences: {},
|
||||
isMobile: false,
|
||||
isInitialized: false,
|
||||
updateProfile: async () => {},
|
||||
updatePreferences: async () => {},
|
||||
clearError: () => {}
|
||||
updateProfile: async () => { },
|
||||
updatePreferences: async () => { },
|
||||
clearError: () => { }
|
||||
});
|
||||
|
||||
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
@ -63,7 +66,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let userInfo: User | null = null; // Declare at function scope
|
||||
try {
|
||||
logger.debug('user-context', '🔄 Resolving user profile', {
|
||||
@ -110,30 +113,30 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
email: userInfo.email
|
||||
});
|
||||
|
||||
let profileRow: Record<string, unknown> | null = null;
|
||||
let profileRow: Record<string, unknown> | null = null;
|
||||
|
||||
logger.debug('user-context', '🔧 Step 5: Querying profiles table...', {
|
||||
userId: userInfo.id
|
||||
});
|
||||
|
||||
|
||||
// Set loading state when we start the actual database query
|
||||
setLoading(true);
|
||||
|
||||
|
||||
// Query profiles table without timeout to see actual error
|
||||
logger.debug('user-context', '🔧 Step 5b: Starting profiles query...', {
|
||||
userId: userInfo.id,
|
||||
clientType: 'authenticated'
|
||||
});
|
||||
|
||||
|
||||
// Try direct fetch instead of Supabase client to bypass hanging issue
|
||||
logger.debug('user-context', '🔧 Step 5b1: About to make profiles query with direct fetch...', {
|
||||
userId: userInfo.id,
|
||||
queryStarted: true
|
||||
});
|
||||
|
||||
const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
|
||||
|
||||
const { data, error } = await fetch(`${supabaseUrl}/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`,
|
||||
'Authorization': `Bearer ${supabaseAnonKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
@ -151,20 +154,20 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
});
|
||||
return { data: null, error: { message: err.message, code: 'FETCH_ERROR' } };
|
||||
});
|
||||
|
||||
|
||||
logger.debug('user-context', '🔧 Step 5b2: Direct fetch completed...', {
|
||||
userId: userInfo.id,
|
||||
hasData: !!data,
|
||||
hasError: !!error
|
||||
});
|
||||
|
||||
|
||||
logger.debug('user-context', '🔧 Step 5c: Profiles query completed', {
|
||||
hasData: !!data,
|
||||
hasError: !!error,
|
||||
errorCode: error?.code,
|
||||
errorMessage: error?.message
|
||||
});
|
||||
|
||||
|
||||
logger.debug('user-context', '🔧 Step 5a: Profiles query result', {
|
||||
hasData: !!data,
|
||||
hasError: !!error,
|
||||
@ -316,7 +319,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
userId: userInfo?.id,
|
||||
email: userInfo?.email
|
||||
});
|
||||
|
||||
|
||||
if (userInfo) {
|
||||
const metadata = userInfo.user_metadata as CCUserMetadata;
|
||||
const fallbackProfile: CCUser = {
|
||||
@ -330,12 +333,12 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
created_at: userInfo.created_at,
|
||||
updated_at: userInfo.updated_at
|
||||
};
|
||||
|
||||
|
||||
DatabaseNameService.rememberDatabaseNames({
|
||||
userDbName: fallbackProfile.user_db_name,
|
||||
schoolDbName: fallbackProfile.school_db_name
|
||||
});
|
||||
|
||||
|
||||
setProfile(fallbackProfile);
|
||||
logger.debug('user-context', '✅ Fallback profile created', {
|
||||
userId: fallbackProfile.id,
|
||||
@ -345,7 +348,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} else {
|
||||
setProfile(null);
|
||||
}
|
||||
|
||||
|
||||
setPreferences({});
|
||||
setError(error instanceof Error ? error : new Error('Failed to load user profile'));
|
||||
setLoading(false); // Ensure loading is cleared on error
|
||||
@ -353,12 +356,12 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
logger.debug('user-context', '🔧 Finalizing user context initialization...', {
|
||||
isMounted: mountedRef.current
|
||||
});
|
||||
|
||||
|
||||
if (mountedRef.current) {
|
||||
// Loading state is already managed above, just log completion
|
||||
logger.debug('user-context', '✅ User context initialization complete');
|
||||
}
|
||||
|
||||
|
||||
logger.debug('user-context', '🔧 Step 10: Setting isInitialized to true');
|
||||
setIsInitialized(true);
|
||||
logger.debug('user-context', '✅ User context initialized flag set - initialization complete!', {
|
||||
|
||||
@ -110,7 +110,8 @@ export type LogCategory =
|
||||
| 'auth-service'
|
||||
| 'user-context'
|
||||
| 'neo-user-context'
|
||||
| 'neo-institute-context';
|
||||
| 'neo-institute-context'
|
||||
| 'search-service';
|
||||
|
||||
interface LogConfig {
|
||||
enabled: boolean; // Master switch to turn logging on/off
|
||||
@ -338,6 +339,7 @@ logger.setConfig({
|
||||
'user-context',
|
||||
'neo-user-context',
|
||||
'neo-institute-context',
|
||||
'search-service'
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -42,10 +42,10 @@ import {
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { supabase } from '../../supabaseClient';
|
||||
import {
|
||||
pickDirectory,
|
||||
processDirectoryFiles,
|
||||
calculateDirectoryStats,
|
||||
import {
|
||||
pickDirectory,
|
||||
processDirectoryFiles,
|
||||
calculateDirectoryStats,
|
||||
formatFileSize,
|
||||
isDirectoryPickerSupported,
|
||||
FileWithPath
|
||||
@ -104,7 +104,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
const [files, setFiles] = useState<FileRecord[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
// Pagination and filtering state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
@ -128,7 +128,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
const apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record<string, string> }) => {
|
||||
const session = await supabase.auth.getSession();
|
||||
const token = session?.data?.session?.access_token;
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No authentication token available');
|
||||
}
|
||||
@ -140,12 +140,12 @@ const SimpleUploadTest: React.FC = () => {
|
||||
|
||||
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]);
|
||||
|
||||
@ -171,7 +171,7 @@ const SimpleUploadTest: 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
|
||||
@ -183,20 +183,20 @@ const SimpleUploadTest: React.FC = () => {
|
||||
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)`
|
||||
|
||||
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);
|
||||
@ -240,7 +240,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
// Single file upload
|
||||
const handleSingleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !selectedCabinet) return;
|
||||
|
||||
|
||||
const file = e.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('cabinet_id', selectedCabinet);
|
||||
@ -250,22 +250,22 @@ const SimpleUploadTest: React.FC = () => {
|
||||
|
||||
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}`
|
||||
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `File uploaded successfully using ${uploadType === 'new' ? 'NEW' : 'OLD'} endpoint: ${file.name}`
|
||||
});
|
||||
|
||||
|
||||
await loadFiles(selectedCabinet);
|
||||
e.target.value = '';
|
||||
} catch (error: unknown) {
|
||||
@ -311,14 +311,14 @@ const SimpleUploadTest: React.FC = () => {
|
||||
|
||||
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);
|
||||
};
|
||||
@ -327,61 +327,61 @@ const SimpleUploadTest: React.FC = () => {
|
||||
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
|
||||
|
||||
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)`
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@ -404,12 +404,12 @@ const SimpleUploadTest: React.FC = () => {
|
||||
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) {
|
||||
@ -434,7 +434,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
<Typography variant="h4" gutterBottom>
|
||||
🧪 Simple Upload Test Page
|
||||
</Typography>
|
||||
|
||||
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<Typography variant="body2">
|
||||
This page tests the NEW simple upload system (no auto-processing) vs the OLD system (with auto-processing).
|
||||
@ -452,8 +452,8 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{/* Upload Controls */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Upload Controls"
|
||||
<CardHeader
|
||||
title="Upload Controls"
|
||||
avatar={<UploadIcon />}
|
||||
/>
|
||||
<CardContent>
|
||||
@ -504,15 +504,15 @@ const SimpleUploadTest: React.FC = () => {
|
||||
Upload File
|
||||
</Button>
|
||||
|
||||
<input
|
||||
<input
|
||||
ref={dirInputRef}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
{...({ webkitdirectory: '' } as any)}
|
||||
multiple
|
||||
multiple
|
||||
onChange={handleFallbackDirectorySelect}
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FolderOpenIcon />}
|
||||
@ -552,8 +552,8 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{/* System Info */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="System Info"
|
||||
<CardHeader
|
||||
title="System Info"
|
||||
avatar={<InfoIcon />}
|
||||
/>
|
||||
<CardContent>
|
||||
@ -594,8 +594,8 @@ const SimpleUploadTest: React.FC = () => {
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Upload Mode:</strong>
|
||||
</Typography>
|
||||
<Chip
|
||||
label={uploadType === 'new' ? 'NEW (Simple)' : 'OLD (Auto-Processing)'}
|
||||
<Chip
|
||||
label={uploadType === 'new' ? 'NEW (Simple)' : 'OLD (Auto-Processing)'}
|
||||
color={uploadType === 'new' ? 'success' : 'warning'}
|
||||
/>
|
||||
</Box>
|
||||
@ -629,7 +629,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{/* File List */}
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
<CardHeader
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
@ -656,7 +656,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
sx={{ minWidth: 200 }}
|
||||
/>
|
||||
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>Sort by</InputLabel>
|
||||
<Select
|
||||
@ -670,7 +670,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
<MenuItem value="processing_status">Status</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>Order</InputLabel>
|
||||
<Select
|
||||
@ -682,7 +682,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
<MenuItem value="desc">Descending</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>Per page</InputLabel>
|
||||
<Select
|
||||
@ -702,12 +702,12 @@ const SimpleUploadTest: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
{/* File List with Fixed Height */}
|
||||
<Box sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
<Box sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
height: 400, // Fixed height
|
||||
overflow: 'auto'
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{loading ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
@ -757,9 +757,9 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{file.name}
|
||||
</Typography>
|
||||
{file.is_directory && <Chip label="Directory" size="small" />}
|
||||
<Chip
|
||||
label={file.processing_status || 'unknown'}
|
||||
size="small"
|
||||
<Chip
|
||||
label={file.processing_status || 'unknown'}
|
||||
size="small"
|
||||
color={getStatusColor(file.processing_status)}
|
||||
/>
|
||||
</Box>
|
||||
@ -789,7 +789,7 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{pagination && pagination.total_pages > 1 && (
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Pagination
|
||||
<Pagination
|
||||
count={pagination.total_pages}
|
||||
page={pagination.page}
|
||||
onChange={(event, value) => setCurrentPage(value)}
|
||||
@ -817,18 +817,18 @@ const SimpleUploadTest: React.FC = () => {
|
||||
{isUploading && <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: 300, overflow: 'auto' }}>
|
||||
{uploadProgress.map((item, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', py: 0.5 }}>
|
||||
@ -838,31 +838,31 @@ const SimpleUploadTest: React.FC = () => {
|
||||
<Typography variant="body2" sx={{ mr: 2, minWidth: 80 }}>
|
||||
{formatFileSize(item.size)}
|
||||
</Typography>
|
||||
<Chip
|
||||
<Chip
|
||||
label={item.status}
|
||||
size="small"
|
||||
color={
|
||||
item.status === 'done' ? 'success' :
|
||||
item.status === 'error' ? 'error' :
|
||||
item.status === 'uploading' ? 'primary' : 'default'
|
||||
item.status === 'done' ? 'success' :
|
||||
item.status === 'error' ? 'error' :
|
||||
item.status === 'uploading' ? 'primary' : 'default'
|
||||
}
|
||||
icon={
|
||||
item.status === 'done' ? <SuccessIcon /> :
|
||||
item.status === 'error' ? <ErrorIcon /> : undefined
|
||||
item.status === 'done' ? <SuccessIcon /> :
|
||||
item.status === 'error' ? <ErrorIcon /> : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
</DialogContent>
|
||||
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowUploadDialog(false)} disabled={isUploading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={startDirectoryUpload}
|
||||
variant="contained"
|
||||
<Button
|
||||
onClick={startDirectoryUpload}
|
||||
variant="contained"
|
||||
disabled={isUploading || selectedFiles.length === 0}
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Start Upload'}
|
||||
|
||||
@ -4,7 +4,7 @@ import { HEADER_HEIGHT } from './Layout';
|
||||
|
||||
const SearxngPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: HEADER_HEIGHT,
|
||||
left: 0,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import logger from "../../debugConfig"
|
||||
|
||||
interface SearXNGResult {
|
||||
title?: string
|
||||
url?: string
|
||||
@ -26,19 +28,25 @@ export class SearchService {
|
||||
engines: 'google,bing,duckduckgo',
|
||||
})
|
||||
|
||||
const url = `${import.meta.env.VITE_FRONTEND_SITE_URL}/searxng-api/search?${searchParams.toString()}`
|
||||
const url = `/searxng-api/search?${searchParams.toString()}`
|
||||
logger.debug('search-service', "Search URL: ", url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
logger.debug('search-service', "Search response: ", response)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed with status: ${response.status}`)
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
logger.debug('search-service', "Search data: ", data)
|
||||
|
||||
return (data.results || []).map((result: SearXNGResult) => ({
|
||||
title: result.title || '',
|
||||
url: result.url || '',
|
||||
|
||||
36
src/sw.ts
36
src/sw.ts
@ -40,13 +40,13 @@ const manifestIndexEntry = manifest.find(entry => {
|
||||
|
||||
// Create index entries using the revision from manifest if available
|
||||
const indexEntries = [
|
||||
{
|
||||
url: '/index.html',
|
||||
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
|
||||
{
|
||||
url: '/index.html',
|
||||
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
|
||||
},
|
||||
{
|
||||
url: '/',
|
||||
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
|
||||
{
|
||||
url: '/',
|
||||
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
|
||||
}
|
||||
];
|
||||
|
||||
@ -92,12 +92,12 @@ const navigationHandler = async ({ request }: { request: Request }): Promise<Res
|
||||
|
||||
// If network fails, try cache
|
||||
const cache = await caches.open(CACHE_NAMES.pages);
|
||||
|
||||
|
||||
// Try both root and /index.html paths
|
||||
const cachedResponse = await cache.match(request) ||
|
||||
await cache.match('/') ||
|
||||
await cache.match('/index.html');
|
||||
|
||||
await cache.match('/') ||
|
||||
await cache.match('/index.html');
|
||||
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
@ -151,8 +151,8 @@ registerRoute(
|
||||
|
||||
// Cache manifest and icons with a Stale While Revalidate strategy
|
||||
registerRoute(
|
||||
({ request }) =>
|
||||
request.destination === 'manifest' ||
|
||||
({ request }) =>
|
||||
request.destination === 'manifest' ||
|
||||
request.url.includes('/icons/'),
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'manifest-and-icons',
|
||||
@ -168,10 +168,10 @@ registerRoute(
|
||||
registerRoute(
|
||||
({ request }) => {
|
||||
const destination = request.destination;
|
||||
return destination === 'style' ||
|
||||
destination === 'script' ||
|
||||
destination === 'image' ||
|
||||
destination === 'font'
|
||||
return destination === 'style' ||
|
||||
destination === 'script' ||
|
||||
destination === 'image' ||
|
||||
destination === 'font'
|
||||
},
|
||||
new CacheFirst({
|
||||
cacheName: CACHE_NAMES.static,
|
||||
@ -209,7 +209,7 @@ registerRoute(
|
||||
|
||||
// Cache SearXNG API with Network First strategy
|
||||
registerRoute(
|
||||
({ url }) => url.pathname.startsWith('/searxng-api'),
|
||||
({ url }) => url.pathname.startsWith('/searxng-api/'),
|
||||
new NetworkFirst({
|
||||
cacheName: CACHE_NAMES.api,
|
||||
plugins: [
|
||||
@ -227,7 +227,7 @@ registerRoute(
|
||||
|
||||
// Cache SearXNG static assets
|
||||
registerRoute(
|
||||
({ url }) => url.pathname.startsWith('/searxng-api/static'),
|
||||
({ url }) => url.pathname.startsWith('/searxng-api/static/'),
|
||||
new CacheFirst({
|
||||
cacheName: CACHE_NAMES.static,
|
||||
plugins: [
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -9,10 +9,10 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||
export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {
|
||||
// Load env file based on mode in correct order
|
||||
const env = loadEnv(mode, process.cwd(), 'VITE_');
|
||||
|
||||
|
||||
// Determine base URL from hostname
|
||||
const base = '/'; // Always use root path, let nginx handle the routing
|
||||
|
||||
|
||||
// Determine if we're in production based on mode and VITE_DEV flag
|
||||
const isProd = mode === 'production' && env.VITE_DEV !== 'true';
|
||||
|
||||
@ -87,7 +87,7 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
swDest: 'dist/sw.js',
|
||||
manifestTransforms: [
|
||||
// Transform manifest entries to ensure proper caching
|
||||
(entries) => ({
|
||||
(entries: any[]) => ({
|
||||
manifest: entries.map(entry => ({
|
||||
...entry,
|
||||
url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}`
|
||||
@ -106,6 +106,8 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
'import.meta.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
|
||||
'import.meta.env.VITE_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL),
|
||||
'import.meta.env.VITE_DEV': env.VITE_DEV === 'true',
|
||||
'import.meta.env.VITE_API_BASE': JSON.stringify(env.VITE_API_BASE),
|
||||
'import.meta.env.VITE_SEARCH_URL': JSON.stringify(env.VITE_SEARCH_URL),
|
||||
},
|
||||
envPrefix: 'VITE_',
|
||||
base,
|
||||
@ -125,6 +127,13 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
port: parseInt(env.VITE_PORT_FRONTEND || '5173'),
|
||||
clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'),
|
||||
overlay: false
|
||||
},
|
||||
proxy: {
|
||||
'/searxng-api': {
|
||||
target: env.VITE_SEARCH_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/searxng-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
clearScreen: false,
|
||||
@ -143,12 +152,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
} : undefined,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
||||
'vendor-mui': ['@mui/material', '@mui/icons-material'],
|
||||
'vendor-tldraw': ['@tldraw/tldraw', '@tldraw/store', '@tldraw/tlschema'],
|
||||
'vendor-utils': ['axios', 'zustand', '@supabase/supabase-js']
|
||||
},
|
||||
// Ensure chunk filenames include content hash
|
||||
chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js',
|
||||
assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user