diff --git a/components/Landing.tsx b/components/Landing.tsx index a1b56c0..8e69678 100644 --- a/components/Landing.tsx +++ b/components/Landing.tsx @@ -1,16 +1,14 @@ import React, { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, X, FileText, Image, ScanText, Sparkles } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { BrainCircuit, Loader2, Play, PenTool, BookOpen } from 'lucide-react'; import { useAuth } from 'react-oidc-context'; import { AuthButton } from './AuthButton'; import { QuizLibrary } from './QuizLibrary'; import { useQuizLibrary } from '../hooks/useQuizLibrary'; import type { Quiz } from '../types'; -type GenerateMode = 'topic' | 'document'; - interface LandingProps { - onGenerate: (options: { topic?: string; questionCount?: number; files?: File[]; useOcr?: boolean }) => void; + onGenerate: (topic: string) => void; onCreateManual: () => void; onLoadQuiz: (quiz: Quiz) => void; onJoin: (pin: string, name: string) => void; @@ -21,21 +19,11 @@ interface LandingProps { export const Landing: React.FC = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => { const auth = useAuth(); const [mode, setMode] = useState<'HOST' | 'JOIN'>('HOST'); - const [generateMode, setGenerateMode] = useState('topic'); const [topic, setTopic] = useState(''); const [pin, setPin] = useState(''); const [name, setName] = useState(''); const [libraryOpen, setLibraryOpen] = useState(false); - const [selectedFiles, setSelectedFiles] = useState([]); - const [questionCount, setQuestionCount] = useState(10); - const [isDragging, setIsDragging] = useState(false); - const [useOcr, setUseOcr] = useState(false); - - const hasImageFile = selectedFiles.some(f => f.type.startsWith('image/')); - const hasDocumentFile = selectedFiles.some(f => !f.type.startsWith('image/') && !['application/pdf', 'text/plain', 'text/markdown', 'text/csv', 'text/html'].includes(f.type)); - const showOcrOption = hasImageFile || hasDocumentFile; - const { quizzes, loading: libraryLoading, @@ -54,62 +42,9 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on } }, [libraryOpen, auth.isAuthenticated, fetchQuizzes]); - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - const newFiles = Array.from(e.target.files); - setSelectedFiles(prev => [...prev, ...newFiles]); - } - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const acceptedTypes = ['.pdf', '.txt', '.md', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods', '.rtf', '.jpg', '.jpeg', '.png', '.gif', '.webp']; - const newFiles = Array.from(e.dataTransfer.files).filter((file: File) => { - const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); - return acceptedTypes.includes(fileExtension) || file.type.startsWith('image/'); - }); - if (newFiles.length > 0) { - setSelectedFiles(prev => [...prev, ...newFiles]); - } - } - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - }; - - const removeFile = (index: number) => { - setSelectedFiles(prev => prev.filter((_, i) => i !== index)); - if (selectedFiles.length <= 1) { - setUseOcr(false); - } - }; - - const clearAllFiles = () => { - setSelectedFiles([]); - setUseOcr(false); - }; - - const canGenerate = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; - const handleHostSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (canGenerate && !isLoading) { - onGenerate({ - topic: generateMode === 'topic' ? topic.trim() : undefined, - questionCount, - files: generateMode === 'document' ? selectedFiles : undefined, - useOcr: generateMode === 'document' && showOcrOption ? useOcr : undefined - }); - } + if (topic.trim()) onGenerate(topic); }; const handleJoinSubmit = (e: React.FormEvent) => { @@ -165,187 +100,22 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on {mode === 'HOST' ? ( -
+
-
- - -
- - - {generateMode === 'topic' ? ( - - setTopic(e.target.value)} - className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" - disabled={isLoading} - /> - - ) : ( - -
document.getElementById('file-upload')?.click()} - className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${ - isDragging - ? 'border-theme-primary bg-theme-primary/5 scale-[1.02]' - : selectedFiles.length > 0 - ? 'border-theme-primary/50 bg-theme-primary/5' - : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' - }`} - > - - -
- -

- {isDragging ? 'Drop files here' : 'Drop files or click to browse'} -

-

PDF, DOCX, PPTX, XLSX, TXT, Images

-
-
- - {selectedFiles.length > 0 && ( -
-
- {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected - -
-
- {selectedFiles.map((file, index) => ( -
-
-
- {file.type.startsWith('image/') ? ( - - ) : ( - - )} -
- {file.name} -
- -
- ))} -
-
- )} - - {showOcrOption && ( - - )} -
- )} -
- -
-
- Questions - {questionCount} -
- setQuestionCount(Number(e.target.value))} - className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" - /> -
- + setTopic(e.target.value)} + className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" + disabled={isLoading} + />
diff --git a/docker-compose.yml b/docker-compose.yml index 0b0a3f7..0c001fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,7 +110,7 @@ services: OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/ CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173} volumes: - - ./data:/data + - kaboot-data:/data ports: - "${KABOOT_BACKEND_PORT:-3001}:3001" depends_on: @@ -121,6 +121,7 @@ services: volumes: postgresql-data: redis-data: + kaboot-data: networks: kaboot-network: diff --git a/hooks/useGame.ts b/hooks/useGame.ts index 259cc53..5bda1be 100644 --- a/hooks/useGame.ts +++ b/hooks/useGame.ts @@ -1,11 +1,9 @@ import { useState, useEffect, useRef, useCallback } from 'react'; -import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument } from '../types'; +import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question } from '../types'; import { generateQuiz } from '../services/geminiService'; import { POINTS_PER_QUESTION, QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS } from '../constants'; import { Peer, DataConnection } from 'peerjs'; -const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001'; - export const useGame = () => { const [role, setRole] = useState('HOST'); const [gameState, setGameState] = useState('LANDING'); @@ -45,50 +43,16 @@ export const useGame = () => { // -- HOST LOGIC -- - const uploadDocument = async (file: File, useOcr: boolean = false): Promise => { - const formData = new FormData(); - formData.append('document', file); - formData.append('useOcr', String(useOcr)); - - const response = await fetch(`${BACKEND_URL}/api/upload`, { - method: 'POST', - body: formData - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to upload document'); - } - - return response.json(); - }; - - const startQuizGen = async (options: { topic?: string; questionCount?: number; files?: File[]; useOcr?: boolean }) => { + const startQuizGen = async (topic: string) => { try { setGameState('GENERATING'); setError(null); setRole('HOST'); - - let documents: ProcessedDocument[] | undefined; - if (options.files && options.files.length > 0) { - documents = await Promise.all( - options.files.map(file => uploadDocument(file, options.useOcr)) - ); - } - - const generateOptions: GenerateQuizOptions = { - topic: options.topic, - questionCount: options.questionCount, - documents - }; - - const generatedQuiz = await generateQuiz(generateOptions); - const saveLabel = options.topic || options.files?.map(f => f.name).join(', ') || ''; - setPendingQuizToSave({ quiz: generatedQuiz, topic: saveLabel }); + const generatedQuiz = await generateQuiz(topic); + setPendingQuizToSave({ quiz: generatedQuiz, topic }); initializeHostGame(generatedQuiz); } catch (e) { - const message = e instanceof Error ? e.message : "Failed to generate quiz."; - setError(message); + setError("Failed to generate quiz."); setGameState('LANDING'); } }; @@ -265,16 +229,10 @@ export const useGame = () => { } if (timerRef.current) clearInterval(timerRef.current); - let tickCount = 0; timerRef.current = setInterval(() => { setTimeLeft(prev => { if (prev <= 100) { endQuestion(); return 0; } - const newTime = prev - 100; - tickCount++; - if (tickCount % 10 === 0) { - broadcast({ type: 'TIME_SYNC', payload: { timeLeft: newTime } }); - } - return newTime; + return prev - 100; }); }, 100); }; @@ -378,12 +336,7 @@ export const useGame = () => { } } - if (data.type === 'TIME_SYNC') { - setTimeLeft(data.payload.timeLeft); - } - if (data.type === 'TIME_UP') { - if (timerRef.current) clearInterval(timerRef.current); setGameState('REVEAL'); } diff --git a/server/package-lock.json b/server/package-lock.json index 53c3e55..4ea764d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,8 +13,6 @@ "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "multer": "^2.0.2", - "officeparser": "^6.0.4", "uuid": "^11.0.5" }, "devDependencies": { @@ -22,7 +20,6 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jsonwebtoken": "^9.0.7", - "@types/multer": "^2.0.0", "@types/node": "^22.10.7", "@types/uuid": "^10.0.0", "tsx": "^4.19.2", @@ -471,262 +468,6 @@ "node": ">=18" } }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.88.tgz", - "integrity": "sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg==", - "license": "MIT", - "optional": true, - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.88", - "@napi-rs/canvas-darwin-arm64": "0.1.88", - "@napi-rs/canvas-darwin-x64": "0.1.88", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.88", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.88", - "@napi-rs/canvas-linux-arm64-musl": "0.1.88", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.88", - "@napi-rs/canvas-linux-x64-gnu": "0.1.88", - "@napi-rs/canvas-linux-x64-musl": "0.1.88", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.88", - "@napi-rs/canvas-win32-x64-msvc": "0.1.88" - } - }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.88.tgz", - "integrity": "sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.88.tgz", - "integrity": "sha512-Xgywz0dDxOKSgx3eZnK85WgGMmGrQEW7ZLA/E7raZdlEE+xXCozobgqz2ZvYigpB6DJFYkqnwHjqCOTSDGlFdg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.88.tgz", - "integrity": "sha512-Yz4wSCIQOUgNucgk+8NFtQxQxZV5NO8VKRl9ePKE6XoNyNVC8JDqtvhh3b3TPqKK8W5p2EQpAr1rjjm0mfBxdg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.88.tgz", - "integrity": "sha512-9gQM2SlTo76hYhxHi2XxWTAqpTOb+JtxMPEIr+H5nAhHhyEtNmTSDRtz93SP7mGd2G3Ojf2oF5tP9OdgtgXyKg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.88.tgz", - "integrity": "sha512-7qgaOBMXuVRk9Fzztzr3BchQKXDxGbY+nwsovD3I/Sx81e+sX0ReEDYHTItNb0Je4NHbAl7D0MKyd4SvUc04sg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.88.tgz", - "integrity": "sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.88.tgz", - "integrity": "sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.88.tgz", - "integrity": "sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.88.tgz", - "integrity": "sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.88.tgz", - "integrity": "sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.88.tgz", - "integrity": "sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -819,16 +560,6 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, - "node_modules/@types/multer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", - "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/node": { "version": "22.19.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz", @@ -878,27 +609,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -912,12 +622,6 @@ "node": ">= 0.6" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -975,12 +679,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/bmp-js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", - "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -1029,38 +727,12 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1105,21 +777,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1364,24 +1021,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1437,23 +1076,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1650,12 +1272,6 @@ "node": ">=0.10.0" } }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1697,12 +1313,6 @@ "node": ">= 0.10" } }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "license": "MIT" - }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -2011,18 +1621,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2035,24 +1633,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -2080,26 +1660,6 @@ "node": ">=10" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2121,26 +1681,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/officeparser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/officeparser/-/officeparser-6.0.4.tgz", - "integrity": "sha512-zmxst4xbrUxCCY6w5BLmmtewJY5eHkLKNJ+2Z5OKY0JPh5qmtUSslFkg0fC3xc+0RG4DFdfQ36107B99yLMfuQ==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.10", - "concat-stream": "^2.0.0", - "file-type": "^16.5.4", - "pdfjs-dist": "5.4.530", - "tesseract.js": "^6.0.0", - "yauzl": "^3.1.3" - }, - "bin": { - "officeparser": "dist/index.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2162,15 +1702,6 @@ "wrappy": "1" } }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2186,37 +1717,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pdfjs-dist": { - "version": "5.4.530", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz", - "integrity": "sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.84" - } - }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2243,15 +1743,6 @@ "node": ">=10" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2343,68 +1834,6 @@ "node": ">= 6" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", - "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -2630,14 +2059,6 @@ "node": ">= 0.8" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2656,23 +2077,6 @@ "node": ">=0.10.0" } }, - "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -2701,30 +2105,6 @@ "node": ">=6" } }, - "node_modules/tesseract.js": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", - "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "bmp-js": "^0.1.0", - "idb-keyval": "^6.2.0", - "is-url": "^1.2.4", - "node-fetch": "^2.6.9", - "opencollective-postinstall": "^2.0.3", - "regenerator-runtime": "^0.13.3", - "tesseract.js-core": "^6.0.0", - "wasm-feature-detect": "^1.2.11", - "zlibjs": "^0.3.1" - } - }, - "node_modules/tesseract.js-core": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.1.2.tgz", - "integrity": "sha512-pv4GjmramjdObhDyR1q85Td8X60Puu/lGQn7Kw2id05LLgHhAcWgnz6xSdMCSxBMWjQDmMyDXPTC2aqADdpiow==", - "license": "Apache-2.0" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2734,29 +2114,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2802,12 +2159,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2874,70 +2225,17 @@ "node": ">= 0.8" } }, - "node_modules/wasm-feature-detect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", - "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "license": "Apache-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" - }, - "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/zlibjs": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", - "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", - "license": "MIT", - "engines": { - "node": "*" - } } } } diff --git a/server/package.json b/server/package.json index 1e412dd..8ba79da 100644 --- a/server/package.json +++ b/server/package.json @@ -16,8 +16,6 @@ "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "multer": "^2.0.2", - "officeparser": "^6.0.4", "uuid": "^11.0.5" }, "devDependencies": { @@ -25,7 +23,6 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jsonwebtoken": "^9.0.7", - "@types/multer": "^2.0.0", "@types/node": "^22.10.7", "@types/uuid": "^10.0.0", "tsx": "^4.19.2", diff --git a/server/src/index.ts b/server/src/index.ts index 886e7a9..4557eba 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,7 +3,6 @@ import cors from 'cors'; import { db } from './db/connection.js'; import quizzesRouter from './routes/quizzes.js'; import usersRouter from './routes/users.js'; -import uploadRouter from './routes/upload.js'; const app = express(); const PORT = process.env.PORT || 3001; @@ -59,7 +58,6 @@ app.get('/health', (_req: Request, res: Response) => { app.use('/api/quizzes', quizzesRouter); app.use('/api/users', usersRouter); -app.use('/api/upload', uploadRouter); app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { console.error('Unhandled error:', err); diff --git a/server/src/routes/upload.ts b/server/src/routes/upload.ts deleted file mode 100644 index c120247..0000000 --- a/server/src/routes/upload.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Router } from 'express'; -import multer from 'multer'; -import { processDocument, SUPPORTED_TYPES } from '../services/documentParser.js'; - -const router = Router(); - -const storage = multer.memoryStorage(); - -const upload = multer({ - storage, - limits: { - fileSize: 50 * 1024 * 1024, // 50MB limit - }, - fileFilter: (_req, file, cb) => { - if (SUPPORTED_TYPES.includes(file.mimetype)) { - cb(null, true); - } else { - cb(new Error(`Unsupported file type: ${file.mimetype}. Supported types: ${SUPPORTED_TYPES.join(', ')}`)); - } - } -}); - -router.post('/', upload.single('document'), async (req, res) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const useOcr = req.body?.useOcr === 'true' || req.body?.useOcr === true; - const processed = await processDocument(req.file.buffer, req.file.mimetype, { useOcr }); - - if (processed.type === 'native') { - res.json({ - type: 'native', - content: (processed.content as Buffer).toString('base64'), - mimeType: processed.mimeType, - originalName: req.file.originalname - }); - } else { - res.json({ - type: 'text', - content: processed.content as string, - mimeType: processed.mimeType, - originalName: req.file.originalname - }); - } - } catch (error) { - console.error('Upload processing error:', error); - res.status(500).json({ - error: error instanceof Error ? error.message : 'Failed to process document' - }); - } -}); - -router.use((err: Error, _req: any, res: any, _next: any) => { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' }); - } - return res.status(400).json({ error: err.message }); - } - - if (err.message.includes('Unsupported file type')) { - return res.status(400).json({ error: err.message }); - } - - console.error('Upload error:', err); - res.status(500).json({ error: 'Upload failed' }); -}); - -export default router; diff --git a/server/src/services/documentParser.ts b/server/src/services/documentParser.ts deleted file mode 100644 index ad72b6f..0000000 --- a/server/src/services/documentParser.ts +++ /dev/null @@ -1,123 +0,0 @@ -import officeParser from 'officeparser'; - -// MIME types that Gemini can handle natively (send as-is) -export const GEMINI_NATIVE_TYPES = [ - 'application/pdf', - 'text/plain', - 'text/markdown', - 'text/csv', - 'text/html', - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp' -]; - -// MIME types that officeparser can extract text from -export const OFFICEPARSER_TYPES = [ - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx - 'application/vnd.oasis.opendocument.text', // .odt - 'application/vnd.oasis.opendocument.presentation', // .odp - 'application/vnd.oasis.opendocument.spreadsheet', // .ods - 'application/rtf', // .rtf -]; - -// Image types that can use OCR for text extraction -export const OCR_CAPABLE_TYPES = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp' -]; - -export const SUPPORTED_TYPES = [...GEMINI_NATIVE_TYPES, ...OFFICEPARSER_TYPES]; - -export interface ProcessedDocument { - type: 'native' | 'text'; - content: Buffer | string; - mimeType: string; -} - -export interface ProcessOptions { - useOcr?: boolean; -} - -export function isSupportedType(mimeType: string): boolean { - return SUPPORTED_TYPES.includes(mimeType); -} - -export function isGeminiNative(mimeType: string): boolean { - return GEMINI_NATIVE_TYPES.includes(mimeType); -} - -export function isOcrCapable(mimeType: string): boolean { - return OCR_CAPABLE_TYPES.includes(mimeType); -} - -export function needsOfficeParser(mimeType: string): boolean { - return OFFICEPARSER_TYPES.includes(mimeType); -} - -async function extractWithOfficeParser(buffer: Buffer, useOcr: boolean = false): Promise { - const config = useOcr ? { - extractAttachments: true, - ocr: true, - ocrLanguage: 'eng' - } : {}; - - const ast = await officeParser.parseOffice(buffer, config); - let text = ast.toText(); - - if (useOcr && ast.attachments) { - for (const attachment of ast.attachments) { - if (attachment.ocrText) { - text += '\n' + attachment.ocrText; - } - } - } - - return text; -} - -export async function processDocument( - buffer: Buffer, - mimeType: string, - options: ProcessOptions = {} -): Promise { - if (!isSupportedType(mimeType)) { - throw new Error(`Unsupported file type: ${mimeType}`); - } - - // Images with OCR requested - extract text - if (isOcrCapable(mimeType) && options.useOcr) { - const text = await extractWithOfficeParser(buffer, true); - return { - type: 'text', - content: text, - mimeType: 'text/plain' - }; - } - - // Gemini-native types (including images without OCR) - pass through - if (isGeminiNative(mimeType)) { - return { - type: 'native', - content: buffer, - mimeType - }; - } - - // Office documents - extract text, OCR extracts text from embedded images - if (needsOfficeParser(mimeType)) { - const text = await extractWithOfficeParser(buffer, options.useOcr); - return { - type: 'text', - content: text, - mimeType: 'text/plain' - }; - } - - throw new Error(`No extraction handler for: ${mimeType}`); -} diff --git a/services/geminiService.ts b/services/geminiService.ts index 5e1cd18..4e4131a 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -1,5 +1,5 @@ -import { GoogleGenAI, Type, createUserContent, createPartFromUri } from "@google/genai"; -import { Quiz, Question, AnswerOption, GenerateQuizOptions, ProcessedDocument } from "../types"; +import { GoogleGenAI, Type } from "@google/genai"; +import { Quiz, Question, AnswerOption } from "../types"; import { v4 as uuidv4 } from 'uuid'; const getClient = () => { @@ -10,62 +10,61 @@ const getClient = () => { return new GoogleGenAI({ apiKey }); }; -const QUIZ_SCHEMA = { - type: Type.OBJECT, - properties: { - title: { type: Type.STRING, description: "A catchy title for the quiz" }, - questions: { - type: Type.ARRAY, - items: { +export const generateQuiz = async (topic: string): Promise => { + const ai = getClient(); + + const prompt = `Generate a trivia quiz about "${topic}". Create 10 engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty. For each option, provide a brief reason explaining why it is correct or incorrect - this helps players learn.`; + + const response = await ai.models.generateContent({ + model: "gemini-3-flash-preview", + contents: prompt, + config: { + responseMimeType: "application/json", + responseSchema: { type: Type.OBJECT, properties: { - text: { type: Type.STRING, description: "The question text" }, - options: { + title: { type: Type.STRING, description: "A catchy title for the quiz" }, + questions: { type: Type.ARRAY, items: { type: Type.OBJECT, properties: { - text: { type: Type.STRING }, - isCorrect: { type: Type.BOOLEAN }, - reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" } + text: { type: Type.STRING, description: "The question text" }, + options: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + text: { type: Type.STRING }, + isCorrect: { type: Type.BOOLEAN }, + reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" } + }, + required: ["text", "isCorrect", "reason"] + }, + } }, - required: ["text", "isCorrect", "reason"] - }, + required: ["text", "options"] + } } }, - required: ["text", "options"] + required: ["title", "questions"] } } - }, - required: ["title", "questions"] -}; + }); -function buildPrompt(options: GenerateQuizOptions, hasDocuments: boolean): string { - const questionCount = options.questionCount || 10; - - const baseInstructions = `Create ${questionCount} engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty. - -IMPORTANT: For each option's reason, write as if you are directly explaining facts - never reference "the document", "the text", "the material", or "the source". Write explanations as standalone factual statements.`; - - if (hasDocuments) { - const topicContext = options.topic - ? ` Focus on aspects related to "${options.topic}".` - : ''; - return `Generate a quiz based on the provided content.${topicContext} - -${baseInstructions}`; + if (!response.text) { + throw new Error("Failed to generate quiz content"); } - - return `Generate a trivia quiz about "${options.topic}". -${baseInstructions}`; -} - -function transformToQuiz(data: any): Quiz { - const shapes = ['triangle', 'diamond', 'circle', 'square'] as const; - const colors = ['red', 'blue', 'yellow', 'green'] as const; + const data = JSON.parse(response.text); + // Transform to our internal type with shapes/colors pre-assigned const questions: Question[] = data.questions.map((q: any) => { + const shapes = ['triangle', 'diamond', 'circle', 'square'] as const; + const colors = ['red', 'blue', 'yellow', 'green'] as const; + + // Shuffle options so the correct one isn't always first (though Gemini usually randomizes, safety first) + // Actually, to map shapes consistently, let's keep array order but assign props const options: AnswerOption[] = q.options.map((opt: any, index: number) => ({ text: opt.text, isCorrect: opt.isCorrect, @@ -86,67 +85,4 @@ function transformToQuiz(data: any): Quiz { title: data.title, questions }; -} - -async function uploadNativeDocument(ai: GoogleGenAI, doc: ProcessedDocument): Promise<{ uri: string; mimeType: string }> { - const buffer = typeof doc.content === 'string' - ? Buffer.from(doc.content, 'base64') - : doc.content; - - const blob = new Blob([buffer], { type: doc.mimeType }); - - const uploadedFile = await ai.files.upload({ - file: blob, - config: { mimeType: doc.mimeType! } - }); - - if (!uploadedFile.uri || !uploadedFile.mimeType) { - throw new Error("Failed to upload document to Gemini"); - } - - return { uri: uploadedFile.uri, mimeType: uploadedFile.mimeType }; -} - -export const generateQuiz = async (options: GenerateQuizOptions): Promise => { - const ai = getClient(); - - const docs = options.documents || []; - const hasDocuments = docs.length > 0; - const prompt = buildPrompt(options, hasDocuments); - - let contents: any; - - if (hasDocuments) { - const parts: any[] = []; - - for (const doc of docs) { - if (doc.type === 'native' && doc.mimeType) { - const uploaded = await uploadNativeDocument(ai, doc); - parts.push(createPartFromUri(uploaded.uri, uploaded.mimeType)); - } else if (doc.type === 'text') { - parts.push({ text: doc.content as string }); - } - } - - parts.push({ text: prompt }); - contents = createUserContent(parts); - } else { - contents = prompt; - } - - const response = await ai.models.generateContent({ - model: "gemini-3-flash-preview", - contents, - config: { - responseMimeType: "application/json", - responseSchema: QUIZ_SCHEMA - } - }); - - if (!response.text) { - throw new Error("Failed to generate quiz content"); - } - - const data = JSON.parse(response.text); - return transformToQuiz(data); }; diff --git a/tsconfig.json b/tsconfig.json index 21643e7..2c6eed5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,7 @@ ], "skipLibCheck": true, "types": [ - "node", - "vite/client" + "node" ], "moduleResolution": "bundler", "isolatedModules": true, diff --git a/types.ts b/types.ts index c9eb4e6..6fc1adc 100644 --- a/types.ts +++ b/types.ts @@ -51,18 +51,6 @@ export interface QuizListItem { updatedAt: string; } -export interface ProcessedDocument { - type: 'native' | 'text'; - content: string | Buffer; - mimeType?: string; -} - -export interface GenerateQuizOptions { - topic?: string; - questionCount?: number; - documents?: ProcessedDocument[]; -} - export interface Player { id: string; name: string; @@ -94,7 +82,6 @@ export type NetworkMessage = } | { type: 'ANSWER'; payload: { playerId: string; isCorrect: boolean } } | { type: 'RESULT'; payload: { isCorrect: boolean; scoreAdded: number; newScore: number } } - | { type: 'TIME_SYNC'; payload: { timeLeft: number } } | { type: 'TIME_UP'; payload: {} } | { type: 'SHOW_SCOREBOARD'; payload: { players: Player[] } } | { type: 'GAME_OVER'; payload: { players: Player[] } }; \ No newline at end of file