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
.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,11 +28,16 @@ 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 \

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11178
dist/assets/index.css vendored

File diff suppressed because it is too large Load Diff

21232
dist/assets/pdf.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

19850
dist/assets/vendor-mui.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

12
dist/audioWorklet.js vendored
View File

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

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

View File

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

Before

Width:  |  Height:  |  Size: 689 B

18
dist/index.html vendored
View File

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

View File

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

70
dist/offline.html vendored
View File

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

1
dist/registerSW.js vendored
View File

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

3889
dist/sw.js vendored

File diff suppressed because it is too large Load Diff

1
dist/sw.js.map vendored

File diff suppressed because one or more lines are too long

16525
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -57,14 +57,6 @@
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@storybook/addon-actions": "^8.6.12",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-links": "^8.6.12",
"@storybook/addon-onboarding": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/react": "^18.2.66",
@ -90,7 +82,6 @@
"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,6 +8,9 @@ import { DatabaseNameService } from '../services/graph/databaseNameService';
import { provisionUser } from '../services/provisioningService';
import { storageService, StorageKeys } from '../services/auth/localStorageService';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export interface UserContextType {
user: CCUser | null;
loading: boolean;
@ -29,9 +32,9 @@ export const UserContext = createContext<UserContextType>({
preferences: {},
isMobile: false,
isInitialized: false,
updateProfile: async () => {},
updatePreferences: async () => {},
clearError: () => {}
updateProfile: async () => { },
updatePreferences: async () => { },
clearError: () => { }
});
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
@ -131,9 +134,9 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
queryStarted: true
});
const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
const { data, error } = await fetch(`${supabaseUrl}/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
headers: {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`,
'Authorization': `Bearer ${supabaseAnonKey}`,
'Content-Type': 'application/json'
}
})

View File

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

View File

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

View File

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

View File

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

View File

@ -139,7 +139,7 @@ export const CCFilesPanel: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>;
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = {
@ -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 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',
manifestTransforms: [
// Transform manifest entries to ensure proper caching
(entries) => ({
(entries: any[]) => ({
manifest: entries.map(entry => ({
...entry,
url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}`
@ -106,6 +106,8 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
'import.meta.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
'import.meta.env.VITE_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL),
'import.meta.env.VITE_DEV': env.VITE_DEV === 'true',
'import.meta.env.VITE_API_BASE': JSON.stringify(env.VITE_API_BASE),
'import.meta.env.VITE_SEARCH_URL': JSON.stringify(env.VITE_SEARCH_URL),
},
envPrefix: 'VITE_',
base,
@ -125,6 +127,13 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
port: parseInt(env.VITE_PORT_FRONTEND || '5173'),
clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'),
overlay: false
},
proxy: {
'/searxng-api': {
target: env.VITE_SEARCH_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/searxng-api/, '')
}
}
},
clearScreen: false,
@ -143,12 +152,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
} : undefined,
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-mui': ['@mui/material', '@mui/icons-material'],
'vendor-tldraw': ['@tldraw/tldraw', '@tldraw/store', '@tldraw/tlschema'],
'vendor-utils': ['axios', 'zustand', '@supabase/supabase-js']
},
// Ensure chunk filenames include content hash
chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js',
assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]'