Compare commits

..

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

43 changed files with 224965 additions and 341 deletions

17
.env Normal file
View File

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

5
.gitignore vendored
View File

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

View File

@ -1,4 +1,4 @@
FROM node:20 AS builder
FROM node:20 as builder
WORKDIR /app
COPY package*.json ./
# TODO: Remove this or review embedded variables
@ -28,16 +28,11 @@ RUN echo 'server { \
expires 30d; \
add_header Cache-Control "public, no-transform"; \
} \
location /searxng-api/ { \
proxy_pass https://search.kevlarai.com/; \
proxy_ssl_server_name on; \
proxy_set_header Host search.kevlarai.com; \
} \
location ~ /\. { \
deny all; \
} \
error_page 404 /index.html; \
}' > /etc/nginx/conf.d/default.conf
}' > /etc/nginx/conf.d/default.conf
# Set up permissions
RUN chown -R nginx:nginx /usr/share/nginx/html \

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

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

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

11178
dist/assets/index.css vendored Normal file

File diff suppressed because it is too large Load Diff

21232
dist/assets/pdf.js vendored Normal file

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

12
dist/audioWorklet.js vendored Normal file
View File

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

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

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

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

After

Width:  |  Height:  |  Size: 689 B

18
dist/index.html vendored Normal file
View File

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

1
dist/manifest.webmanifest vendored Normal file
View File

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

70
dist/offline.html vendored Normal file
View File

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

1
dist/registerSW.js vendored Normal file
View File

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

3889
dist/sw.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/sw.js.map vendored Normal file

File diff suppressed because one or more lines are too long

16525
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,14 @@
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@storybook/addon-actions": "^8.6.12",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-links": "^8.6.12",
"@storybook/addon-onboarding": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/react": "^18.2.66",
@ -82,6 +90,7 @@
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"react-refresh": "^0.14.2",
"storybook": "^8.6.12",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite-plugin-pwa": "^0.21.1",

View File

@ -8,9 +8,6 @@ import { DatabaseNameService } from '../services/graph/databaseNameService';
import { provisionUser } from '../services/provisioningService';
import { storageService, StorageKeys } from '../services/auth/localStorageService';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export interface UserContextType {
user: CCUser | null;
loading: boolean;
@ -32,9 +29,9 @@ export const UserContext = createContext<UserContextType>({
preferences: {},
isMobile: false,
isInitialized: false,
updateProfile: async () => { },
updatePreferences: async () => { },
clearError: () => { }
updateProfile: async () => {},
updatePreferences: async () => {},
clearError: () => {}
});
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
@ -134,9 +131,9 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
queryStarted: true
});
const { data, error } = await fetch(`${supabaseUrl}/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
headers: {
'Authorization': `Bearer ${supabaseAnonKey}`,
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`,
'Content-Type': 'application/json'
}
})

View File

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

View File

@ -823,7 +823,7 @@ const SimpleUploadTest: React.FC = () => {
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br />
<strong>{directoryStats.directoryCount} folders</strong><br/>
Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography>
</Alert>

View File

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

View File

@ -209,7 +209,7 @@ registerRoute(
// Cache SearXNG API with Network First strategy
registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api/'),
({ url }) => url.pathname.startsWith('/searxng-api'),
new NetworkFirst({
cacheName: CACHE_NAMES.api,
plugins: [
@ -227,7 +227,7 @@ registerRoute(
// Cache SearXNG static assets
registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api/static/'),
({ url }) => url.pathname.startsWith('/searxng-api/static'),
new CacheFirst({
cacheName: CACHE_NAMES.static,
plugins: [

View File

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

View File

@ -139,7 +139,7 @@ export const CCFilesPanel: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>;
const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = {
@ -387,10 +387,10 @@ export const CCFilesPanel: React.FC = () => {
const iconForMime = (mime?: string, isDirectory?: boolean) => {
if (isDirectory) return <FolderIcon />;
if (!mime) return <InsertDriveFileIcon />;
if (mime.startsWith('image/')) return <ImageIcon />;
if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon />;
return <InsertDriveFileIcon />;
if (!mime) return <InsertDriveFileIcon/>;
if (mime.startsWith('image/')) return <ImageIcon/>;
if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon/>;
return <InsertDriveFileIcon/>;
};
const formatFileSize = (bytes: number): string => {
@ -565,7 +565,7 @@ export const CCFilesPanel: React.FC = () => {
}}>
{loading ? (
<Box sx={{ p: 2, textAlign: 'center' }}>
<CircularProgress size={20} />
<CircularProgress size={20}/>
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
Loading files...
</Typography>
@ -593,10 +593,10 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={
<>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon />
<MoreVertIcon/>
</IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon />
<DeleteIcon/>
</IconButton>
</>
}
@ -632,10 +632,10 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={
<>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon />
<MoreVertIcon/>
</IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon />
<DeleteIcon/>
</IconButton>
</>
}
@ -769,7 +769,7 @@ export const CCFilesPanel: React.FC = () => {
{artefacts.length > 0 && (
<>
<Divider />
<Divider/>
<List dense sx={{
border: '1px solid var(--color-divider)',
borderRadius: '4px',
@ -810,7 +810,7 @@ export const CCFilesPanel: React.FC = () => {
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br />
<strong>{directoryStats.directoryCount} folders</strong><br/>
Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography>
</Alert>

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

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

View File

@ -87,7 +87,7 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
swDest: 'dist/sw.js',
manifestTransforms: [
// Transform manifest entries to ensure proper caching
(entries: any[]) => ({
(entries) => ({
manifest: entries.map(entry => ({
...entry,
url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}`
@ -106,8 +106,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
'import.meta.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
'import.meta.env.VITE_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL),
'import.meta.env.VITE_DEV': env.VITE_DEV === 'true',
'import.meta.env.VITE_API_BASE': JSON.stringify(env.VITE_API_BASE),
'import.meta.env.VITE_SEARCH_URL': JSON.stringify(env.VITE_SEARCH_URL),
},
envPrefix: 'VITE_',
base,
@ -127,13 +125,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
port: parseInt(env.VITE_PORT_FRONTEND || '5173'),
clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'),
overlay: false
},
proxy: {
'/searxng-api': {
target: env.VITE_SEARCH_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/searxng-api/, '')
}
}
},
clearScreen: false,
@ -152,6 +143,12 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
} : undefined,
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-mui': ['@mui/material', '@mui/icons-material'],
'vendor-tldraw': ['@tldraw/tldraw', '@tldraw/store', '@tldraw/tlschema'],
'vendor-utils': ['axios', 'zustand', '@supabase/supabase-js']
},
// Ensure chunk filenames include content hash
chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js',
assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]'