Compare commits

...

2 Commits

43 changed files with 341 additions and 224965 deletions

17
.env
View File

@ -1,17 +0,0 @@
PORT_FRONTEND=5173
PORT_FRONTEND_HMR=3002
PORT_API=800
PORT_SUPABASE=8000
VITE_PORT_FRONTEND=5173
VITE_PORT_FRONTEND_HMR=5173
VITE_APP_NAME=Classroom Copilot
VITE_SUPER_ADMIN_EMAIL=admin@classroomcopilot.ai
VITE_DEV=true
VITE_FRONTEND_SITE_URL=https://app.classroomcopilot.ai
VITE_APP_HMR_URL=https://app.classroomcopilot.ai
VITE_SUPABASE_URL=https://supa.classroomcopilot.ai
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
VITE_API_URL=https://api.classroomcopilot.ai
VITE_API_BASE=https://api.classroomcopilot.ai

7
.gitignore vendored
View File

@ -1,3 +1,8 @@
node_modules
.env
.env
# Build output
dist/
# Lock files - clean install
package-lock.json

View File

@ -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 \

View File

@ -1,59 +0,0 @@
{
"_vendor-mui.js": {
"file": "assets/vendor-mui.js",
"name": "vendor-mui",
"imports": [
"_vendor-react.js"
]
},
"_vendor-react.js": {
"file": "assets/vendor-react.js",
"name": "vendor-react"
},
"_vendor-tldraw.js": {
"file": "assets/vendor-tldraw.js",
"name": "vendor-tldraw",
"imports": [
"_vendor-react.js",
"_vendor-mui.js"
]
},
"_vendor-utils.js": {
"file": "assets/vendor-utils.js",
"name": "vendor-utils",
"imports": [
"_vendor-react.js"
]
},
"index.html": {
"file": "assets/index-CmYeIoD0.js",
"name": "index",
"src": "index.html",
"isEntry": true,
"imports": [
"_vendor-mui.js",
"_vendor-react.js",
"_vendor-tldraw.js",
"_vendor-utils.js"
],
"dynamicImports": [
"node_modules/pdfjs-dist/build/pdf.mjs"
],
"css": [
"assets/index.css"
],
"assets": [
"assets/pdf.worker.min.mjs"
]
},
"node_modules/pdfjs-dist/build/pdf.mjs": {
"file": "assets/pdf.js",
"name": "pdf",
"src": "node_modules/pdfjs-dist/build/pdf.mjs",
"isDynamicEntry": true
},
"node_modules/pdfjs-dist/build/pdf.worker.min.mjs": {
"file": "assets/pdf.worker.min.mjs",
"src": "node_modules/pdfjs-dist/build/pdf.worker.min.mjs"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11178
dist/assets/index.css vendored

File diff suppressed because it is too large Load Diff

21232
dist/assets/pdf.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

19850
dist/assets/vendor-mui.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

12
dist/audioWorklet.js vendored
View File

@ -1,12 +0,0 @@
class AudioProcessor extends AudioWorkletProcessor {
process(inputs) {
const input = inputs[0];
if (input.length > 0) {
const audioData = input[0];
this.port.postMessage(audioData);
}
return true;
}
}
registerProcessor('audio-processor', AudioProcessor);

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Sticker outline -->
<path d="M20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Peeling corner effect -->
<path d="M16 8C16 10.2091 14.2091 12 12 12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Star decoration -->
<path d="M12 8L13 9L12 10L11 9L12 8Z"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 689 B

18
dist/index.html vendored
View File

@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Classroom Copilot</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<script type="module" crossorigin src="/assets/index-CmYeIoD0.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-mui.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-tldraw.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-utils.js">
<link rel="stylesheet" crossorigin href="/assets/index.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1 +0,0 @@
{"name":"ClassroomCopilot","short_name":"CC","start_url":"/","display":"fullscreen","background_color":"#ffffff","theme_color":"#000000","lang":"en","scope":"/","icons":[{"src":"/icons/icon-192x192.png","sizes":"192x192","type":"image/png","purpose":"any"},{"src":"/icons/icon-512x512.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"/icons/icon-192x192-maskable.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"/icons/icon-512x512-maskable.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}

70
dist/offline.html vendored
View File

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - Classroom Copilot</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f5f5f5;
color: #333;
text-align: center;
}
.container {
max-width: 600px;
padding: 40px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #1a73e8;
margin-bottom: 20px;
}
p {
line-height: 1.6;
margin-bottom: 20px;
}
.retry-button {
background-color: #1a73e8;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.retry-button:hover {
background-color: #1557b0;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">📡</div>
<h1>You're Offline</h1>
<p>It looks like you've lost your internet connection. Don't worry - any work you've done has been saved locally.</p>
<p>Please check your connection and try again.</p>
<button class="retry-button" onclick="window.location.reload()">Try Again</button>
</div>
<script>
// Check if we're back online
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>

1
dist/registerSW.js vendored
View File

@ -1 +0,0 @@
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})}

3889
dist/sw.js vendored

File diff suppressed because it is too large Load Diff

1
dist/sw.js.map vendored

File diff suppressed because one or more lines are too long

16525
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}
}

View File

@ -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!', {

View File

@ -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'
],
});

View File

@ -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'}

View File

@ -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,

View File

@ -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 || '',

View File

@ -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: [

View File

@ -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 => (

View File

@ -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'}

48
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// App Information
readonly VITE_APP_NAME: string
readonly VITE_APP_VERSION: string
readonly VITE_APP_DESCRIPTION: string
readonly VITE_APP_AUTHOR: string
// Super Admin Email
readonly VITE_SUPER_ADMIN_EMAIL: string
// Supabase
readonly VITE_SUPABASE_PUBLIC_URL: string
readonly VITE_SUPABASE_URL: string
readonly VITE_SUPABASE_REDIRECT_URL: string
readonly VITE_SUPABASE_ANON_KEY: string
// Site URL
readonly VITE_FRONTEND_SITE_URL: string
// Firebase Settings
readonly VITE_REACT_APP_API_KEY: string
readonly VITE_REACT_APP_AUTH_DOMAIN: string
readonly VITE_REACT_APP_PROJECT_ID: string
readonly VITE_REACT_APP_STORAGE_BUCKET: string
readonly VITE_REACT_APP_MESSAGING_SENDER_ID: string
readonly VITE_REACT_APP_APP_ID: string
// Supabase Settings
// Microsoft API Settings
readonly VITE_MICROSOFT_CLIENT_ID: string
readonly VITE_MICROSOFT_CLIENT_SECRET_DESC: string
readonly VITE_MICROSOFT_CLIENT_SECRET_ID: string
readonly VITE_MICROSOFT_CLIENT_SECRET: string
readonly VITE_MICROSOFT_TENANT_ID: string
// API Base
readonly VITE_API_BASE: string
// Search URL
readonly VITE_SEARCH_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -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]'