Compare commits

..

No commits in common. "5c100a015dddb4f5891e7d13d79e2bcd7a0e3a00" and "3472f203b94abc5430007f319c376fdd53d3b31a" have entirely different histories.

43 changed files with 224965 additions and 341 deletions

17
.env Normal file
View File

@ -0,0 +1,17 @@
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,8 +1,3 @@
node_modules 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 WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# TODO: Remove this or review embedded variables # TODO: Remove this or review embedded variables
@ -20,24 +20,19 @@ RUN echo 'server { \
root /usr/share/nginx/html; \ root /usr/share/nginx/html; \
index index.html; \ index index.html; \
location / { \ location / { \
try_files $uri $uri/ /index.html; \ try_files $uri $uri/ /index.html; \
expires 30d; \ expires 30d; \
add_header Cache-Control "public, no-transform"; \ add_header Cache-Control "public, no-transform"; \
} \ } \
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \
expires 30d; \ expires 30d; \
add_header Cache-Control "public, no-transform"; \ 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 ~ /\. { \ location ~ /\. { \
deny all; \ deny all; \
} \ } \
error_page 404 /index.html; \ error_page 404 /index.html; \
}' > /etc/nginx/conf.d/default.conf }' > /etc/nginx/conf.d/default.conf
# Set up permissions # Set up permissions
RUN chown -R nginx:nginx /usr/share/nginx/html \ RUN chown -R nginx:nginx /usr/share/nginx/html \

59
dist/.vite/manifest.json vendored Normal file
View File

@ -0,0 +1,59 @@
{
"_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"
}
}

68827
dist/assets/index-CmYeIoD0.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index-CmYeIoD0.js.map vendored Normal file

File diff suppressed because one or more lines are too long

11178
dist/assets/index.css vendored Normal file

File diff suppressed because it is too large Load Diff

21232
dist/assets/pdf.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/assets/pdf.js.map vendored Normal file

File diff suppressed because one or more lines are too long

21
dist/assets/pdf.worker.min.mjs vendored Normal file

File diff suppressed because one or more lines are too long

19850
dist/assets/vendor-mui.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/assets/vendor-mui.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1901
dist/assets/vendor-react.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/assets/vendor-react.js.map vendored Normal file

File diff suppressed because one or more lines are too long

68796
dist/assets/vendor-tldraw.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/vendor-tldraw.js.map vendored Normal file

File diff suppressed because one or more lines are too long

12264
dist/assets/vendor-utils.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/assets/vendor-utils.js.map vendored Normal file

File diff suppressed because one or more lines are too long

12
dist/audioWorklet.js vendored Normal file
View File

@ -0,0 +1,12 @@
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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
dist/icons/icon-192x192-maskable.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
dist/icons/icon-192x192.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
dist/icons/icon-512x512-maskable.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
dist/icons/icon-512x512.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

21
dist/icons/sticker-tool.svg vendored Normal file
View File

@ -0,0 +1,21 @@
<?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>

After

Width:  |  Height:  |  Size: 689 B

18
dist/index.html vendored Normal file
View File

@ -0,0 +1,18 @@
<!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>

1
dist/manifest.webmanifest vendored Normal file
View File

@ -0,0 +1 @@
{"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 Normal file
View File

@ -0,0 +1,70 @@
<!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 Normal file
View File

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

3889
dist/sw.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/sw.js.map vendored Normal file

File diff suppressed because one or more lines are too long

16525
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,14 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.15.0", "@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/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
@ -82,9 +90,10 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"react-refresh": "^0.14.2", "react-refresh": "^0.14.2",
"storybook": "^8.6.12",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite-plugin-pwa": "^0.21.1", "vite-plugin-pwa": "^0.21.1",
"vitest": "^1.6.0" "vitest": "^1.6.0"
} }
} }

View File

@ -8,9 +8,6 @@ import { DatabaseNameService } from '../services/graph/databaseNameService';
import { provisionUser } from '../services/provisioningService'; import { provisionUser } from '../services/provisioningService';
import { storageService, StorageKeys } from '../services/auth/localStorageService'; 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 { export interface UserContextType {
user: CCUser | null; user: CCUser | null;
loading: boolean; loading: boolean;
@ -32,9 +29,9 @@ export const UserContext = createContext<UserContextType>({
preferences: {}, preferences: {},
isMobile: false, isMobile: false,
isInitialized: false, isInitialized: false,
updateProfile: async () => { }, updateProfile: async () => {},
updatePreferences: async () => { }, updatePreferences: async () => {},
clearError: () => { } clearError: () => {}
}); });
export const UserProvider = ({ children }: { children: React.ReactNode }) => { export const UserProvider = ({ children }: { children: React.ReactNode }) => {
@ -66,7 +63,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
return; return;
} }
} }
let userInfo: User | null = null; // Declare at function scope let userInfo: User | null = null; // Declare at function scope
try { try {
logger.debug('user-context', '🔄 Resolving user profile', { logger.debug('user-context', '🔄 Resolving user profile', {
@ -113,30 +110,30 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
email: userInfo.email 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...', { logger.debug('user-context', '🔧 Step 5: Querying profiles table...', {
userId: userInfo.id userId: userInfo.id
}); });
// Set loading state when we start the actual database query // Set loading state when we start the actual database query
setLoading(true); setLoading(true);
// Query profiles table without timeout to see actual error // Query profiles table without timeout to see actual error
logger.debug('user-context', '🔧 Step 5b: Starting profiles query...', { logger.debug('user-context', '🔧 Step 5b: Starting profiles query...', {
userId: userInfo.id, userId: userInfo.id,
clientType: 'authenticated' clientType: 'authenticated'
}); });
// Try direct fetch instead of Supabase client to bypass hanging issue // 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...', { logger.debug('user-context', '🔧 Step 5b1: About to make profiles query with direct fetch...', {
userId: userInfo.id, userId: userInfo.id,
queryStarted: true queryStarted: true
}); });
const { data, error } = await fetch(`${supabaseUrl}/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, { const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
headers: { headers: {
'Authorization': `Bearer ${supabaseAnonKey}`, 'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}) })
@ -154,20 +151,20 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
}); });
return { data: null, error: { message: err.message, code: 'FETCH_ERROR' } }; return { data: null, error: { message: err.message, code: 'FETCH_ERROR' } };
}); });
logger.debug('user-context', '🔧 Step 5b2: Direct fetch completed...', { logger.debug('user-context', '🔧 Step 5b2: Direct fetch completed...', {
userId: userInfo.id, userId: userInfo.id,
hasData: !!data, hasData: !!data,
hasError: !!error hasError: !!error
}); });
logger.debug('user-context', '🔧 Step 5c: Profiles query completed', { logger.debug('user-context', '🔧 Step 5c: Profiles query completed', {
hasData: !!data, hasData: !!data,
hasError: !!error, hasError: !!error,
errorCode: error?.code, errorCode: error?.code,
errorMessage: error?.message errorMessage: error?.message
}); });
logger.debug('user-context', '🔧 Step 5a: Profiles query result', { logger.debug('user-context', '🔧 Step 5a: Profiles query result', {
hasData: !!data, hasData: !!data,
hasError: !!error, hasError: !!error,
@ -319,7 +316,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
userId: userInfo?.id, userId: userInfo?.id,
email: userInfo?.email email: userInfo?.email
}); });
if (userInfo) { if (userInfo) {
const metadata = userInfo.user_metadata as CCUserMetadata; const metadata = userInfo.user_metadata as CCUserMetadata;
const fallbackProfile: CCUser = { const fallbackProfile: CCUser = {
@ -333,12 +330,12 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
created_at: userInfo.created_at, created_at: userInfo.created_at,
updated_at: userInfo.updated_at updated_at: userInfo.updated_at
}; };
DatabaseNameService.rememberDatabaseNames({ DatabaseNameService.rememberDatabaseNames({
userDbName: fallbackProfile.user_db_name, userDbName: fallbackProfile.user_db_name,
schoolDbName: fallbackProfile.school_db_name schoolDbName: fallbackProfile.school_db_name
}); });
setProfile(fallbackProfile); setProfile(fallbackProfile);
logger.debug('user-context', '✅ Fallback profile created', { logger.debug('user-context', '✅ Fallback profile created', {
userId: fallbackProfile.id, userId: fallbackProfile.id,
@ -348,7 +345,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
} else { } else {
setProfile(null); setProfile(null);
} }
setPreferences({}); setPreferences({});
setError(error instanceof Error ? error : new Error('Failed to load user profile')); setError(error instanceof Error ? error : new Error('Failed to load user profile'));
setLoading(false); // Ensure loading is cleared on error setLoading(false); // Ensure loading is cleared on error
@ -356,12 +353,12 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
logger.debug('user-context', '🔧 Finalizing user context initialization...', { logger.debug('user-context', '🔧 Finalizing user context initialization...', {
isMounted: mountedRef.current isMounted: mountedRef.current
}); });
if (mountedRef.current) { if (mountedRef.current) {
// Loading state is already managed above, just log completion // Loading state is already managed above, just log completion
logger.debug('user-context', '✅ User context initialization complete'); logger.debug('user-context', '✅ User context initialization complete');
} }
logger.debug('user-context', '🔧 Step 10: Setting isInitialized to true'); logger.debug('user-context', '🔧 Step 10: Setting isInitialized to true');
setIsInitialized(true); setIsInitialized(true);
logger.debug('user-context', '✅ User context initialized flag set - initialization complete!', { logger.debug('user-context', '✅ User context initialized flag set - initialization complete!', {

View File

@ -110,8 +110,7 @@ export type LogCategory =
| 'auth-service' | 'auth-service'
| 'user-context' | 'user-context'
| 'neo-user-context' | 'neo-user-context'
| 'neo-institute-context' | 'neo-institute-context';
| 'search-service';
interface LogConfig { interface LogConfig {
enabled: boolean; // Master switch to turn logging on/off enabled: boolean; // Master switch to turn logging on/off
@ -339,7 +338,6 @@ logger.setConfig({
'user-context', 'user-context',
'neo-user-context', 'neo-user-context',
'neo-institute-context', 'neo-institute-context',
'search-service'
], ],
}); });

View File

@ -42,10 +42,10 @@ import {
Info as InfoIcon Info as InfoIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { supabase } from '../../supabaseClient'; import { supabase } from '../../supabaseClient';
import { import {
pickDirectory, pickDirectory,
processDirectoryFiles, processDirectoryFiles,
calculateDirectoryStats, calculateDirectoryStats,
formatFileSize, formatFileSize,
isDirectoryPickerSupported, isDirectoryPickerSupported,
FileWithPath FileWithPath
@ -104,7 +104,7 @@ const SimpleUploadTest: React.FC = () => {
const [files, setFiles] = useState<FileRecord[]>([]); const [files, setFiles] = useState<FileRecord[]>([]);
const [pagination, setPagination] = useState<PaginationInfo | null>(null); const [pagination, setPagination] = useState<PaginationInfo | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Pagination and filtering state // Pagination and filtering state
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10); 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 apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record<string, string> }) => {
const session = await supabase.auth.getSession(); const session = await supabase.auth.getSession();
const token = session?.data?.session?.access_token; const token = session?.data?.session?.access_token;
if (!token) { if (!token) {
throw new Error('No authentication token available'); 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 fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`;
const res = await fetch(fullUrl, { ...init, headers }); const res = await fetch(fullUrl, { ...init, headers });
if (!res.ok) { if (!res.ok) {
const errorText = await res.text(); const errorText = await res.text();
throw new Error(`HTTP ${res.status}: ${errorText}`); throw new Error(`HTTP ${res.status}: ${errorText}`);
} }
return res.json(); return res.json();
}, [API_BASE]); }, [API_BASE]);
@ -171,7 +171,7 @@ const SimpleUploadTest: React.FC = () => {
const loadFiles = useCallback(async (cabinetId: string, page: number = currentPage) => { const loadFiles = useCallback(async (cabinetId: string, page: number = currentPage) => {
if (!cabinetId) return; if (!cabinetId) return;
setLoading(true); setLoading(true);
try { try {
// Build query parameters for pagination, search, and sorting // Build query parameters for pagination, search, and sorting
@ -183,20 +183,20 @@ const SimpleUploadTest: React.FC = () => {
sort_order: sortOrder, sort_order: sortOrder,
include_directories: 'true' include_directories: 'true'
}); });
if (searchTerm) { if (searchTerm) {
params.append('search', searchTerm); params.append('search', searchTerm);
} }
// Use the new simple upload endpoint for listing files with pagination // Use the new simple upload endpoint for listing files with pagination
const data: FileListResponse = await apiFetch(`/simple-upload/files?${params.toString()}`); const data: FileListResponse = await apiFetch(`/simple-upload/files?${params.toString()}`);
setFiles(data.files || []); setFiles(data.files || []);
setPagination(data.pagination); setPagination(data.pagination);
setMessage({ setMessage({
type: 'success', type: 'success',
text: `Loaded ${data.files?.length || 0} files (${data.pagination.total_count} total)` text: `Loaded ${data.files?.length || 0} files (${data.pagination.total_count} total)`
}); });
} catch (error: unknown) { } catch (error: unknown) {
console.error('Failed to load files:', error); console.error('Failed to load files:', error);
@ -240,7 +240,7 @@ const SimpleUploadTest: React.FC = () => {
// Single file upload // Single file upload
const handleSingleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleSingleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !selectedCabinet) return; if (!e.target.files || !selectedCabinet) return;
const file = e.target.files[0]; const file = e.target.files[0];
const formData = new FormData(); const formData = new FormData();
formData.append('cabinet_id', selectedCabinet); formData.append('cabinet_id', selectedCabinet);
@ -250,22 +250,22 @@ const SimpleUploadTest: React.FC = () => {
try { try {
setLoading(true); setLoading(true);
// Choose endpoint based on upload type // Choose endpoint based on upload type
const endpoint = uploadType === 'new' ? '/simple-upload/files/upload' : '/database/files/upload'; const endpoint = uploadType === 'new' ? '/simple-upload/files/upload' : '/database/files/upload';
const result = await apiFetch(endpoint, { const result = await apiFetch(endpoint, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
console.log('Upload result:', result); console.log('Upload result:', result);
setMessage({ setMessage({
type: 'success', type: 'success',
text: `File uploaded successfully using ${uploadType === 'new' ? 'NEW' : 'OLD'} endpoint: ${file.name}` text: `File uploaded successfully using ${uploadType === 'new' ? 'NEW' : 'OLD'} endpoint: ${file.name}`
}); });
await loadFiles(selectedCabinet); await loadFiles(selectedCabinet);
e.target.value = ''; e.target.value = '';
} catch (error: unknown) { } catch (error: unknown) {
@ -311,14 +311,14 @@ const SimpleUploadTest: React.FC = () => {
setSelectedFiles(files); setSelectedFiles(files);
setDirectoryStats(calculateDirectoryStats(files)); setDirectoryStats(calculateDirectoryStats(files));
const progress: UploadProgress[] = files.map(file => ({ const progress: UploadProgress[] = files.map(file => ({
path: file.relativePath, path: file.relativePath,
size: file.size, size: file.size,
status: 'queued', status: 'queued',
progress: 0 progress: 0
})); }));
setUploadProgress(progress); setUploadProgress(progress);
setShowUploadDialog(true); setShowUploadDialog(true);
}; };
@ -327,61 +327,61 @@ const SimpleUploadTest: React.FC = () => {
if (!selectedCabinet || selectedFiles.length === 0) return; if (!selectedCabinet || selectedFiles.length === 0) return;
setIsUploading(true); setIsUploading(true);
try { try {
const firstFilePath = selectedFiles[0].relativePath; const firstFilePath = selectedFiles[0].relativePath;
const directoryName = firstFilePath.split('/')[0] || 'uploaded-folder'; const directoryName = firstFilePath.split('/')[0] || 'uploaded-folder';
const formData = new FormData(); const formData = new FormData();
formData.append('cabinet_id', selectedCabinet); formData.append('cabinet_id', selectedCabinet);
formData.append('scope', 'teacher'); formData.append('scope', 'teacher');
formData.append('directory_name', directoryName); formData.append('directory_name', directoryName);
selectedFiles.forEach(file => { selectedFiles.forEach(file => {
formData.append('files', file); formData.append('files', file);
}); });
const relativePaths = selectedFiles.map(f => f.relativePath); const relativePaths = selectedFiles.map(f => f.relativePath);
formData.append('file_paths', JSON.stringify(relativePaths)); formData.append('file_paths', JSON.stringify(relativePaths));
const result = await apiFetch('/simple-upload/files/upload-directory', { const result = await apiFetch('/simple-upload/files/upload-directory', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
console.log('Directory upload result:', result); console.log('Directory upload result:', result);
setUploadProgress(prev => prev.map(item => ({ setUploadProgress(prev => prev.map(item => ({
...item, ...item,
status: 'done', status: 'done',
progress: 100 progress: 100
}))); })));
setMessage({ setMessage({
type: 'success', type: 'success',
text: `Directory uploaded successfully: ${directoryName} (${selectedFiles.length} files)` text: `Directory uploaded successfully: ${directoryName} (${selectedFiles.length} files)`
}); });
await loadFiles(selectedCabinet); await loadFiles(selectedCabinet);
setTimeout(() => { setTimeout(() => {
setShowUploadDialog(false); setShowUploadDialog(false);
setIsUploading(false); setIsUploading(false);
setSelectedFiles([]); setSelectedFiles([]);
setUploadProgress([]); setUploadProgress([]);
}, 2000); }, 2000);
} catch (error: unknown) { } catch (error: unknown) {
console.error('Directory upload failed:', error); console.error('Directory upload failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setMessage({ type: 'error', text: `Directory upload failed: ${errorMessage}` }); setMessage({ type: 'error', text: `Directory upload failed: ${errorMessage}` });
setUploadProgress(prev => prev.map(item => ({ setUploadProgress(prev => prev.map(item => ({
...item, ...item,
status: 'error', status: 'error',
error: String(error) error: String(error)
}))); })));
setIsUploading(false); setIsUploading(false);
} }
}; };
@ -404,12 +404,12 @@ const SimpleUploadTest: React.FC = () => {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('processing_type', 'basic'); formData.append('processing_type', 'basic');
const result = await apiFetch(`/simple-upload/files/${fileId}/process-manual`, { const result = await apiFetch(`/simple-upload/files/${fileId}/process-manual`, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
console.log('Manual processing result:', result); console.log('Manual processing result:', result);
setMessage({ type: 'info', text: 'Manual processing triggered (not yet implemented)' }); setMessage({ type: 'info', text: 'Manual processing triggered (not yet implemented)' });
} catch (error: unknown) { } catch (error: unknown) {
@ -434,7 +434,7 @@ const SimpleUploadTest: React.FC = () => {
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
🧪 Simple Upload Test Page 🧪 Simple Upload Test Page
</Typography> </Typography>
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body2"> <Typography variant="body2">
This page tests the NEW simple upload system (no auto-processing) vs the OLD system (with auto-processing). 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 */} {/* Upload Controls */}
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Card> <Card>
<CardHeader <CardHeader
title="Upload Controls" title="Upload Controls"
avatar={<UploadIcon />} avatar={<UploadIcon />}
/> />
<CardContent> <CardContent>
@ -504,15 +504,15 @@ const SimpleUploadTest: React.FC = () => {
Upload File Upload File
</Button> </Button>
<input <input
ref={dirInputRef} ref={dirInputRef}
type="file" type="file"
style={{ display: 'none' }} style={{ display: 'none' }}
{...({ webkitdirectory: '' } as any)} {...({ webkitdirectory: '' } as any)}
multiple multiple
onChange={handleFallbackDirectorySelect} onChange={handleFallbackDirectorySelect}
/> />
<Button <Button
variant="outlined" variant="outlined"
startIcon={<FolderOpenIcon />} startIcon={<FolderOpenIcon />}
@ -552,8 +552,8 @@ const SimpleUploadTest: React.FC = () => {
{/* System Info */} {/* System Info */}
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Card> <Card>
<CardHeader <CardHeader
title="System Info" title="System Info"
avatar={<InfoIcon />} avatar={<InfoIcon />}
/> />
<CardContent> <CardContent>
@ -594,8 +594,8 @@ const SimpleUploadTest: React.FC = () => {
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
<strong>Upload Mode:</strong> <strong>Upload Mode:</strong>
</Typography> </Typography>
<Chip <Chip
label={uploadType === 'new' ? 'NEW (Simple)' : 'OLD (Auto-Processing)'} label={uploadType === 'new' ? 'NEW (Simple)' : 'OLD (Auto-Processing)'}
color={uploadType === 'new' ? 'success' : 'warning'} color={uploadType === 'new' ? 'success' : 'warning'}
/> />
</Box> </Box>
@ -629,7 +629,7 @@ const SimpleUploadTest: React.FC = () => {
{/* File List */} {/* File List */}
<Grid item xs={12}> <Grid item xs={12}>
<Card> <Card>
<CardHeader <CardHeader
title={ title={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@ -656,7 +656,7 @@ const SimpleUploadTest: React.FC = () => {
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
sx={{ minWidth: 200 }} sx={{ minWidth: 200 }}
/> />
<FormControl size="small" sx={{ minWidth: 120 }}> <FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Sort by</InputLabel> <InputLabel>Sort by</InputLabel>
<Select <Select
@ -670,7 +670,7 @@ const SimpleUploadTest: React.FC = () => {
<MenuItem value="processing_status">Status</MenuItem> <MenuItem value="processing_status">Status</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small" sx={{ minWidth: 100 }}> <FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel>Order</InputLabel> <InputLabel>Order</InputLabel>
<Select <Select
@ -682,7 +682,7 @@ const SimpleUploadTest: React.FC = () => {
<MenuItem value="desc">Descending</MenuItem> <MenuItem value="desc">Descending</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small" sx={{ minWidth: 100 }}> <FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel>Per page</InputLabel> <InputLabel>Per page</InputLabel>
<Select <Select
@ -702,12 +702,12 @@ const SimpleUploadTest: React.FC = () => {
</Box> </Box>
{/* File List with Fixed Height */} {/* File List with Fixed Height */}
<Box sx={{ <Box sx={{
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
height: 400, // Fixed height height: 400, // Fixed height
overflow: 'auto' overflow: 'auto'
}}> }}>
{loading ? ( {loading ? (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
@ -757,9 +757,9 @@ const SimpleUploadTest: React.FC = () => {
{file.name} {file.name}
</Typography> </Typography>
{file.is_directory && <Chip label="Directory" size="small" />} {file.is_directory && <Chip label="Directory" size="small" />}
<Chip <Chip
label={file.processing_status || 'unknown'} label={file.processing_status || 'unknown'}
size="small" size="small"
color={getStatusColor(file.processing_status)} color={getStatusColor(file.processing_status)}
/> />
</Box> </Box>
@ -789,7 +789,7 @@ const SimpleUploadTest: React.FC = () => {
{pagination && pagination.total_pages > 1 && ( {pagination && pagination.total_pages > 1 && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}> <Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Stack spacing={2} alignItems="center"> <Stack spacing={2} alignItems="center">
<Pagination <Pagination
count={pagination.total_pages} count={pagination.total_pages}
page={pagination.page} page={pagination.page}
onChange={(event, value) => setCurrentPage(value)} onChange={(event, value) => setCurrentPage(value)}
@ -817,18 +817,18 @@ const SimpleUploadTest: React.FC = () => {
{isUploading && <LinearProgress sx={{ flexGrow: 1, ml: 2 }} />} {isUploading && <LinearProgress sx={{ flexGrow: 1, ml: 2 }} />}
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
{directoryStats && ( {directoryStats && (
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '} <strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br /> <strong>{directoryStats.directoryCount} folders</strong><br/>
Total size: <strong>{directoryStats.formattedSize}</strong> Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography> </Typography>
</Alert> </Alert>
)} )}
<Paper variant="outlined" sx={{ p: 2, maxHeight: 300, overflow: 'auto' }}> <Paper variant="outlined" sx={{ p: 2, maxHeight: 300, overflow: 'auto' }}>
{uploadProgress.map((item, i) => ( {uploadProgress.map((item, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', py: 0.5 }}> <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 }}> <Typography variant="body2" sx={{ mr: 2, minWidth: 80 }}>
{formatFileSize(item.size)} {formatFileSize(item.size)}
</Typography> </Typography>
<Chip <Chip
label={item.status} label={item.status}
size="small" size="small"
color={ color={
item.status === 'done' ? 'success' : item.status === 'done' ? 'success' :
item.status === 'error' ? 'error' : item.status === 'error' ? 'error' :
item.status === 'uploading' ? 'primary' : 'default' item.status === 'uploading' ? 'primary' : 'default'
} }
icon={ icon={
item.status === 'done' ? <SuccessIcon /> : item.status === 'done' ? <SuccessIcon /> :
item.status === 'error' ? <ErrorIcon /> : undefined item.status === 'error' ? <ErrorIcon /> : undefined
} }
/> />
</Box> </Box>
))} ))}
</Paper> </Paper>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setShowUploadDialog(false)} disabled={isUploading}> <Button onClick={() => setShowUploadDialog(false)} disabled={isUploading}>
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={startDirectoryUpload} onClick={startDirectoryUpload}
variant="contained" variant="contained"
disabled={isUploading || selectedFiles.length === 0} disabled={isUploading || selectedFiles.length === 0}
> >
{isUploading ? 'Uploading...' : 'Start Upload'} {isUploading ? 'Uploading...' : 'Start Upload'}

View File

@ -4,7 +4,7 @@ import { HEADER_HEIGHT } from './Layout';
const SearxngPage: React.FC = () => { const SearxngPage: React.FC = () => {
return ( return (
<Box sx={{ <Box sx={{
position: 'absolute', position: 'absolute',
top: HEADER_HEIGHT, top: HEADER_HEIGHT,
left: 0, left: 0,

View File

@ -1,5 +1,3 @@
import logger from "../../debugConfig"
interface SearXNGResult { interface SearXNGResult {
title?: string title?: string
url?: string url?: string
@ -28,25 +26,19 @@ export class SearchService {
engines: 'google,bing,duckduckgo', engines: 'google,bing,duckduckgo',
}) })
const url = `/searxng-api/search?${searchParams.toString()}` const url = `${import.meta.env.VITE_FRONTEND_SITE_URL}/searxng-api/search?${searchParams.toString()}`
logger.debug('search-service', "Search URL: ", url)
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
}) })
logger.debug('search-service', "Search response: ", response)
if (!response.ok) { if (!response.ok) {
throw new Error(`Search failed with status: ${response.status}`) throw new Error(`Search failed with status: ${response.status}`)
} }
const data = await response.json() const data = await response.json()
logger.debug('search-service', "Search data: ", data)
return (data.results || []).map((result: SearXNGResult) => ({ return (data.results || []).map((result: SearXNGResult) => ({
title: result.title || '', title: result.title || '',
url: result.url || '', url: result.url || '',

View File

@ -40,13 +40,13 @@ const manifestIndexEntry = manifest.find(entry => {
// Create index entries using the revision from manifest if available // Create index entries using the revision from manifest if available
const indexEntries = [ const indexEntries = [
{ {
url: '/index.html', url: '/index.html',
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null
}, },
{ {
url: '/', url: '/',
revision: manifestIndexEntry && typeof manifestIndexEntry !== 'string' ? manifestIndexEntry.revision : null 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 // If network fails, try cache
const cache = await caches.open(CACHE_NAMES.pages); const cache = await caches.open(CACHE_NAMES.pages);
// Try both root and /index.html paths // Try both root and /index.html paths
const cachedResponse = await cache.match(request) || const cachedResponse = await cache.match(request) ||
await cache.match('/') || await cache.match('/') ||
await cache.match('/index.html'); await cache.match('/index.html');
if (cachedResponse) { if (cachedResponse) {
return cachedResponse; return cachedResponse;
} }
@ -151,8 +151,8 @@ registerRoute(
// Cache manifest and icons with a Stale While Revalidate strategy // Cache manifest and icons with a Stale While Revalidate strategy
registerRoute( registerRoute(
({ request }) => ({ request }) =>
request.destination === 'manifest' || request.destination === 'manifest' ||
request.url.includes('/icons/'), request.url.includes('/icons/'),
new StaleWhileRevalidate({ new StaleWhileRevalidate({
cacheName: 'manifest-and-icons', cacheName: 'manifest-and-icons',
@ -168,10 +168,10 @@ registerRoute(
registerRoute( registerRoute(
({ request }) => { ({ request }) => {
const destination = request.destination; const destination = request.destination;
return destination === 'style' || return destination === 'style' ||
destination === 'script' || destination === 'script' ||
destination === 'image' || destination === 'image' ||
destination === 'font' destination === 'font'
}, },
new CacheFirst({ new CacheFirst({
cacheName: CACHE_NAMES.static, cacheName: CACHE_NAMES.static,
@ -209,7 +209,7 @@ registerRoute(
// Cache SearXNG API with Network First strategy // Cache SearXNG API with Network First strategy
registerRoute( registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api/'), ({ url }) => url.pathname.startsWith('/searxng-api'),
new NetworkFirst({ new NetworkFirst({
cacheName: CACHE_NAMES.api, cacheName: CACHE_NAMES.api,
plugins: [ plugins: [
@ -227,7 +227,7 @@ registerRoute(
// Cache SearXNG static assets // Cache SearXNG static assets
registerRoute( registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api/static/'), ({ url }) => url.pathname.startsWith('/searxng-api/static'),
new CacheFirst({ new CacheFirst({
cacheName: CACHE_NAMES.static, cacheName: CACHE_NAMES.static,
plugins: [ plugins: [

View File

@ -25,7 +25,7 @@ export const CCCabinetsPanel: React.FC = () => {
return createTheme({ palette: { mode, divider: 'var(--color-divider)' } }); return createTheme({ palette: { mode, divider: 'var(--color-divider)' } });
}, [tldrawPreferences?.colorScheme, prefersDarkMode]); }, [tldrawPreferences?.colorScheme, prefersDarkMode]);
const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); 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');
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined;
const apiFetch = async (url: string, init?: RequestInitLite) => { const apiFetch = async (url: string, init?: RequestInitLite) => {
@ -75,7 +75,7 @@ export const CCCabinetsPanel: React.FC = () => {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Toolbar> <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> </Toolbar>
<Grid container spacing={1} sx={{ overflow: 'auto' }}> <Grid container spacing={1} sx={{ overflow: 'auto' }}>
{cabinets.map(c => ( {cabinets.map(c => (

View File

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

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

@ -1,48 +0,0 @@
/// <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> => { export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {
// Load env file based on mode in correct order // Load env file based on mode in correct order
const env = loadEnv(mode, process.cwd(), 'VITE_'); const env = loadEnv(mode, process.cwd(), 'VITE_');
// Determine base URL from hostname // Determine base URL from hostname
const base = '/'; // Always use root path, let nginx handle the routing const base = '/'; // Always use root path, let nginx handle the routing
// Determine if we're in production based on mode and VITE_DEV flag // Determine if we're in production based on mode and VITE_DEV flag
const isProd = mode === 'production' && env.VITE_DEV !== 'true'; const isProd = mode === 'production' && env.VITE_DEV !== 'true';
@ -87,7 +87,7 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
swDest: 'dist/sw.js', swDest: 'dist/sw.js',
manifestTransforms: [ manifestTransforms: [
// Transform manifest entries to ensure proper caching // Transform manifest entries to ensure proper caching
(entries: any[]) => ({ (entries) => ({
manifest: entries.map(entry => ({ manifest: entries.map(entry => ({
...entry, ...entry,
url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}` url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}`
@ -106,8 +106,6 @@ 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_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_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL),
'import.meta.env.VITE_DEV': env.VITE_DEV === 'true', '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_', envPrefix: 'VITE_',
base, base,
@ -127,13 +125,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
port: parseInt(env.VITE_PORT_FRONTEND || '5173'), port: parseInt(env.VITE_PORT_FRONTEND || '5173'),
clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'), clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'),
overlay: false overlay: false
},
proxy: {
'/searxng-api': {
target: env.VITE_SEARCH_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/searxng-api/, '')
}
} }
}, },
clearScreen: false, clearScreen: false,
@ -152,6 +143,12 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
} : undefined, } : undefined,
rollupOptions: { rollupOptions: {
output: { 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 // Ensure chunk filenames include content hash
chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js', chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js',
assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]' assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]'