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

5
.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,7 +82,6 @@
"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",

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 }) => {
@ -110,7 +113,7 @@ 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
@ -131,9 +134,9 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
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'
} }
}) })

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

@ -823,7 +823,7 @@ const SimpleUploadTest: React.FC = () => {
<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>
@ -843,12 +843,12 @@ const SimpleUploadTest: React.FC = () => {
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>

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

@ -95,8 +95,8 @@ const navigationHandler = async ({ request }: { request: Request }): Promise<Res
// 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;
@ -169,9 +169,9 @@ 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

@ -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 = {
@ -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 => {
@ -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,10 +593,10 @@ 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>
</> </>
} }
@ -632,10 +632,10 @@ 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>
</> </>
} }
@ -769,7 +769,7 @@ 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',
@ -810,7 +810,7 @@ export const CCFilesPanel: React.FC = () => {
<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>

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

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