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 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,19 +20,24 @@ 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 \

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": { "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",
@ -90,10 +82,9 @@
"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,6 +8,9 @@ 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;
@ -29,9 +32,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 }) => {
@ -63,7 +66,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', {
@ -110,30 +113,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(`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: { headers: {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`, 'Authorization': `Bearer ${supabaseAnonKey}`,
'Content-Type': 'application/json' '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' } }; 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,
@ -316,7 +319,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 = {
@ -330,12 +333,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,
@ -345,7 +348,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
@ -353,12 +356,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,7 +110,8 @@ 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
@ -338,6 +339,7 @@ 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,3 +1,5 @@
import logger from "../../debugConfig"
interface SearXNGResult { interface SearXNGResult {
title?: string title?: string
url?: string url?: string
@ -26,19 +28,25 @@ export class SearchService {
engines: 'google,bing,duckduckgo', 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, { 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 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; 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 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 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 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> => { 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) => ({ (entries: any[]) => ({
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,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_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,
@ -125,6 +127,13 @@ 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,
@ -143,12 +152,6 @@ 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]'