Add document AI gen support
This commit is contained in:
parent
16007cc3aa
commit
028bab23fd
11 changed files with 1270 additions and 170 deletions
|
|
@ -1,14 +1,16 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, ChevronDown, ChevronUp, X, FileText, Settings, Image } from 'lucide-react';
|
import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, X, FileText, Image, ScanText, Sparkles } from 'lucide-react';
|
||||||
import { useAuth } from 'react-oidc-context';
|
import { useAuth } from 'react-oidc-context';
|
||||||
import { AuthButton } from './AuthButton';
|
import { AuthButton } from './AuthButton';
|
||||||
import { QuizLibrary } from './QuizLibrary';
|
import { QuizLibrary } from './QuizLibrary';
|
||||||
import { useQuizLibrary } from '../hooks/useQuizLibrary';
|
import { useQuizLibrary } from '../hooks/useQuizLibrary';
|
||||||
import type { Quiz } from '../types';
|
import type { Quiz } from '../types';
|
||||||
|
|
||||||
|
type GenerateMode = 'topic' | 'document';
|
||||||
|
|
||||||
interface LandingProps {
|
interface LandingProps {
|
||||||
onGenerate: (options: { topic?: string; questionCount?: number; file?: File }) => void;
|
onGenerate: (options: { topic?: string; questionCount?: number; files?: File[]; useOcr?: boolean }) => void;
|
||||||
onCreateManual: () => void;
|
onCreateManual: () => void;
|
||||||
onLoadQuiz: (quiz: Quiz) => void;
|
onLoadQuiz: (quiz: Quiz) => void;
|
||||||
onJoin: (pin: string, name: string) => void;
|
onJoin: (pin: string, name: string) => void;
|
||||||
|
|
@ -19,15 +21,20 @@ interface LandingProps {
|
||||||
export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => {
|
export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [mode, setMode] = useState<'HOST' | 'JOIN'>('HOST');
|
const [mode, setMode] = useState<'HOST' | 'JOIN'>('HOST');
|
||||||
|
const [generateMode, setGenerateMode] = useState<GenerateMode>('topic');
|
||||||
const [topic, setTopic] = useState('');
|
const [topic, setTopic] = useState('');
|
||||||
const [pin, setPin] = useState('');
|
const [pin, setPin] = useState('');
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [libraryOpen, setLibraryOpen] = useState(false);
|
const [libraryOpen, setLibraryOpen] = useState(false);
|
||||||
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||||
const [questionCount, setQuestionCount] = useState(10);
|
const [questionCount, setQuestionCount] = useState(10);
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
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 {
|
const {
|
||||||
quizzes,
|
quizzes,
|
||||||
|
|
@ -48,21 +55,23 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
}, [libraryOpen, auth.isAuthenticated, fetchQuizzes]);
|
}, [libraryOpen, auth.isAuthenticated, fetchQuizzes]);
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
setSelectedFile(e.target.files[0]);
|
const newFiles = Array.from(e.target.files);
|
||||||
|
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
const file = e.dataTransfer.files[0];
|
const acceptedTypes = ['.pdf', '.txt', '.md', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods', '.rtf', '.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||||
const acceptedTypes = ['.pdf', '.txt', '.md', '.docx', '.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
const newFiles = Array.from(e.dataTransfer.files).filter((file: File) => {
|
||||||
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||||
|
return acceptedTypes.includes(fileExtension) || file.type.startsWith('image/');
|
||||||
if (acceptedTypes.includes(fileExtension) || file.type.startsWith('image/')) {
|
});
|
||||||
setSelectedFile(file);
|
if (newFiles.length > 0) {
|
||||||
|
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -77,17 +86,28 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFile = () => {
|
const removeFile = (index: number) => {
|
||||||
setSelectedFile(null);
|
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) => {
|
const handleHostSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if ((topic.trim() || selectedFile) && !isLoading) {
|
if (canGenerate && !isLoading) {
|
||||||
onGenerate({
|
onGenerate({
|
||||||
topic: topic.trim(),
|
topic: generateMode === 'topic' ? topic.trim() : undefined,
|
||||||
questionCount,
|
questionCount,
|
||||||
file: selectedFile || undefined
|
files: generateMode === 'document' ? selectedFiles : undefined,
|
||||||
|
useOcr: generateMode === 'document' && showOcrOption ? useOcr : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -145,120 +165,187 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === 'HOST' ? (
|
{mode === 'HOST' ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
<form onSubmit={handleHostSubmit} className="space-y-4">
|
<form onSubmit={handleHostSubmit} className="space-y-4">
|
||||||
<input
|
<div className="flex bg-gray-100 p-1.5 rounded-xl mb-2">
|
||||||
type="text"
|
|
||||||
placeholder="Topic (e.g. 'Space')"
|
|
||||||
value={topic}
|
|
||||||
onChange={(e) => 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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="border-2 border-gray-100 rounded-2xl overflow-hidden">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
onClick={() => setGenerateMode('topic')}
|
||||||
className="w-full p-3 bg-gray-50 flex items-center justify-center gap-2 text-gray-500 font-bold hover:bg-gray-100 transition-colors"
|
className={`flex-1 py-2 px-3 rounded-lg font-bold text-sm transition-all duration-200 flex items-center justify-center gap-1.5 ${
|
||||||
|
generateMode === 'topic'
|
||||||
|
? 'bg-white shadow-sm text-theme-primary'
|
||||||
|
: 'text-gray-400 hover:text-gray-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Sparkles size={16} /> Topic
|
||||||
Advanced Options
|
|
||||||
{showAdvanced ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
|
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGenerateMode('document')}
|
||||||
|
className={`flex-1 py-2 px-3 rounded-lg font-bold text-sm transition-all duration-200 flex items-center justify-center gap-1.5 ${
|
||||||
|
generateMode === 'document'
|
||||||
|
? 'bg-white shadow-sm text-theme-primary'
|
||||||
|
: 'text-gray-400 hover:text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText size={16} /> Document
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence mode="wait">
|
||||||
{showAdvanced && (
|
{generateMode === 'topic' ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ height: 0, opacity: 0 }}
|
key="topic"
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
exit={{ opacity: 0, y: -10 }}
|
||||||
>
|
transition={{ duration: 0.15 }}
|
||||||
<div className="p-4 space-y-4 bg-white border-t-2 border-gray-100">
|
>
|
||||||
<div className="space-y-2">
|
<input
|
||||||
<div className="flex justify-between items-center text-sm font-bold text-gray-600">
|
type="text"
|
||||||
<span>Question Count</span>
|
placeholder="Enter a topic (e.g. 'Space')"
|
||||||
<span className="bg-theme-primary text-white px-2 py-0.5 rounded-lg text-xs">{questionCount}</span>
|
value={topic}
|
||||||
</div>
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
<input
|
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"
|
||||||
type="range"
|
disabled={isLoading}
|
||||||
min="5"
|
/>
|
||||||
max="30"
|
</motion.div>
|
||||||
value={questionCount}
|
) : (
|
||||||
onChange={(e) => setQuestionCount(Number(e.target.value))}
|
<motion.div
|
||||||
className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
key="document"
|
||||||
/>
|
initial={{ opacity: 0, y: 10 }}
|
||||||
<div className="flex justify-between text-xs text-gray-400 font-medium">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<span>5</span>
|
exit={{ opacity: 0, y: -10 }}
|
||||||
<span>30</span>
|
transition={{ duration: 0.15 }}
|
||||||
</div>
|
className="space-y-3"
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
<div
|
onDrop={handleDrop}
|
||||||
onDrop={handleDrop}
|
onDragOver={handleDragOver}
|
||||||
onDragOver={handleDragOver}
|
onDragLeave={handleDragLeave}
|
||||||
onDragLeave={handleDragLeave}
|
onClick={() => document.getElementById('file-upload')?.click()}
|
||||||
onClick={() => document.getElementById('file-upload')?.click()}
|
className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${
|
||||||
className={`border-2 border-dashed rounded-xl p-4 text-center cursor-pointer transition-all ${
|
isDragging
|
||||||
isDragging
|
? 'border-theme-primary bg-theme-primary/5 scale-[1.02]'
|
||||||
? 'border-theme-primary bg-theme-primary/5 scale-[1.02]'
|
: selectedFiles.length > 0
|
||||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
|
? 'border-theme-primary/50 bg-theme-primary/5'
|
||||||
}`}
|
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
|
||||||
>
|
}`}
|
||||||
<input
|
>
|
||||||
id="file-upload"
|
<input
|
||||||
type="file"
|
id="file-upload"
|
||||||
className="hidden"
|
type="file"
|
||||||
onChange={handleFileSelect}
|
multiple
|
||||||
accept=".pdf,.txt,.md,.docx,.jpg,.jpeg,.png,.gif,.webp"
|
className="hidden"
|
||||||
/>
|
onChange={handleFileSelect}
|
||||||
|
accept=".pdf,.txt,.md,.docx,.pptx,.xlsx,.odt,.odp,.ods,.rtf,.jpg,.jpeg,.png,.gif,.webp"
|
||||||
{selectedFile ? (
|
/>
|
||||||
<div className="flex items-center justify-between bg-gray-100 p-2 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-left">
|
<div className="space-y-2">
|
||||||
{selectedFile.type.startsWith('image/') ? (
|
<Upload className={`mx-auto ${isDragging ? 'text-theme-primary' : 'text-gray-400'}`} size={28} />
|
||||||
<Image size={20} className="text-theme-primary flex-shrink-0" />
|
<p className="text-sm font-bold text-gray-600">
|
||||||
) : (
|
{isDragging ? 'Drop files here' : 'Drop files or click to browse'}
|
||||||
<FileText size={20} className="text-theme-primary flex-shrink-0" />
|
</p>
|
||||||
)}
|
<p className="text-xs text-gray-400">PDF, DOCX, PPTX, XLSX, TXT, Images</p>
|
||||||
<div className="flex flex-col items-start min-w-0">
|
</div>
|
||||||
<span className="text-sm font-bold text-gray-700 truncate max-w-[150px]">{selectedFile.name}</span>
|
</div>
|
||||||
<span className="text-xs text-gray-500">{(selectedFile.size / 1024).toFixed(1)} KB</span>
|
|
||||||
</div>
|
{selectedFiles.length > 0 && (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<button
|
<div className="flex justify-between items-center">
|
||||||
onClick={(e) => {
|
<span className="text-sm font-bold text-gray-600">{selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected</span>
|
||||||
e.stopPropagation();
|
<button
|
||||||
removeFile();
|
type="button"
|
||||||
}}
|
onClick={(e) => {
|
||||||
className="p-1 hover:bg-gray-200 rounded-full text-gray-500 hover:text-red-500 transition-colors"
|
e.stopPropagation();
|
||||||
>
|
clearAllFiles();
|
||||||
<X size={16} />
|
}}
|
||||||
</button>
|
className="text-xs font-bold text-gray-400 hover:text-red-500 transition-colors"
|
||||||
</div>
|
>
|
||||||
) : (
|
Clear all
|
||||||
<div className="space-y-1">
|
</button>
|
||||||
<Upload className={`mx-auto mb-2 ${isDragging ? 'text-theme-primary' : 'text-gray-400'}`} size={24} />
|
</div>
|
||||||
<p className="text-sm font-bold text-gray-600">
|
<div className="max-h-32 overflow-y-auto space-y-1.5">
|
||||||
{isDragging ? 'Drop file here' : 'Upload Document'}
|
{selectedFiles.map((file, index) => (
|
||||||
</p>
|
<div key={index} className="flex items-center justify-between bg-white p-2 rounded-lg shadow-sm">
|
||||||
<p className="text-xs text-gray-400">PDF, TXT, DOCX, Images</p>
|
<div className="flex items-center gap-2 overflow-hidden text-left">
|
||||||
</div>
|
<div className="p-1.5 bg-theme-primary/10 rounded">
|
||||||
)}
|
{file.type.startsWith('image/') ? (
|
||||||
|
<Image size={14} className="text-theme-primary" />
|
||||||
|
) : (
|
||||||
|
<FileText size={14} className="text-theme-primary" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xs font-medium text-gray-700 truncate max-w-[180px]">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeFile(index);
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
|
||||||
|
{showOcrOption && (
|
||||||
|
<label className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl cursor-pointer hover:bg-gray-100 transition-colors">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useOcr}
|
||||||
|
onChange={(e) => setUseOcr(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-10 h-6 bg-gray-300 rounded-full peer-checked:bg-theme-primary transition-colors"></div>
|
||||||
|
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full peer-checked:translate-x-4 transition-transform"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ScanText size={18} className="text-gray-500" />
|
||||||
|
<span className="text-sm font-bold text-gray-600">
|
||||||
|
{hasImageFile && !hasDocumentFile ? 'Extract text with OCR' : 'OCR embedded images'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="space-y-2 pt-2">
|
||||||
|
<div className="flex justify-between items-center text-sm font-bold text-gray-500">
|
||||||
|
<span>Questions</span>
|
||||||
|
<span className="bg-gray-100 text-gray-600 px-2 py-0.5 rounded-lg text-xs">{questionCount}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="30"
|
||||||
|
value={questionCount}
|
||||||
|
onChange={(e) => setQuestionCount(Number(e.target.value))}
|
||||||
|
className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || (!topic.trim() && !selectedFile)}
|
disabled={isLoading || !canGenerate}
|
||||||
className="w-full bg-[#333] text-white py-4 rounded-2xl text-xl font-black shadow-[0_6px_0_#000] active:shadow-none active:translate-y-[6px] transition-all hover:bg-black flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full bg-[#333] text-white py-4 rounded-2xl text-xl font-black shadow-[0_6px_0_#000] active:shadow-none active:translate-y-[6px] transition-all hover:bg-black flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2 className="animate-spin" /> : <><BrainCircuit size={24} /> {selectedFile && !topic.trim() ? 'Generate from Document' : 'Generate Quiz'}</>}
|
{isLoading ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BrainCircuit size={24} />
|
||||||
|
{generateMode === 'document' ? 'Generate from Document' : 'Generate Quiz'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ services:
|
||||||
OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/
|
OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/
|
||||||
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173}
|
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173}
|
||||||
volumes:
|
volumes:
|
||||||
- kaboot-data:/data
|
- ./data:/data
|
||||||
ports:
|
ports:
|
||||||
- "${KABOOT_BACKEND_PORT:-3001}:3001"
|
- "${KABOOT_BACKEND_PORT:-3001}:3001"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -121,7 +121,6 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
postgresql-data:
|
postgresql-data:
|
||||||
redis-data:
|
redis-data:
|
||||||
kaboot-data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
kaboot-network:
|
kaboot-network:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question } from '../types';
|
import { Quiz, Player, GameState, GameRole, NetworkMessage, AnswerOption, Question, GenerateQuizOptions, ProcessedDocument } from '../types';
|
||||||
import { generateQuiz } from '../services/geminiService';
|
import { generateQuiz } from '../services/geminiService';
|
||||||
import { POINTS_PER_QUESTION, QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS } from '../constants';
|
import { POINTS_PER_QUESTION, QUESTION_TIME, QUESTION_TIME_MS, PLAYER_COLORS } from '../constants';
|
||||||
import { Peer, DataConnection } from 'peerjs';
|
import { Peer, DataConnection } from 'peerjs';
|
||||||
|
|
||||||
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
export const useGame = () => {
|
export const useGame = () => {
|
||||||
const [role, setRole] = useState<GameRole>('HOST');
|
const [role, setRole] = useState<GameRole>('HOST');
|
||||||
const [gameState, setGameState] = useState<GameState>('LANDING');
|
const [gameState, setGameState] = useState<GameState>('LANDING');
|
||||||
|
|
@ -43,16 +45,50 @@ export const useGame = () => {
|
||||||
|
|
||||||
// -- HOST LOGIC --
|
// -- HOST LOGIC --
|
||||||
|
|
||||||
const startQuizGen = async (topic: string) => {
|
const uploadDocument = async (file: File, useOcr: boolean = false): Promise<ProcessedDocument> => {
|
||||||
|
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 }) => {
|
||||||
try {
|
try {
|
||||||
setGameState('GENERATING');
|
setGameState('GENERATING');
|
||||||
setError(null);
|
setError(null);
|
||||||
setRole('HOST');
|
setRole('HOST');
|
||||||
const generatedQuiz = await generateQuiz(topic);
|
|
||||||
setPendingQuizToSave({ quiz: generatedQuiz, topic });
|
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 });
|
||||||
initializeHostGame(generatedQuiz);
|
initializeHostGame(generatedQuiz);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError("Failed to generate quiz.");
|
const message = e instanceof Error ? e.message : "Failed to generate quiz.";
|
||||||
|
setError(message);
|
||||||
setGameState('LANDING');
|
setGameState('LANDING');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
702
server/package-lock.json
generated
702
server/package-lock.json
generated
|
|
@ -13,6 +13,8 @@
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"officeparser": "^6.0.4",
|
||||||
"uuid": "^11.0.5"
|
"uuid": "^11.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -20,6 +22,7 @@
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
|
@ -468,6 +471,262 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/@types/better-sqlite3": {
|
||||||
"version": "7.6.13",
|
"version": "7.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
|
|
@ -560,6 +819,16 @@
|
||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.6",
|
"version": "22.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
|
||||||
|
|
@ -609,6 +878,27 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
|
@ -622,6 +912,12 @@
|
||||||
"node": ">= 0.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": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
|
@ -679,6 +975,12 @@
|
||||||
"readable-stream": "^3.4.0"
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.4",
|
"version": "1.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
|
|
@ -727,12 +1029,38 @@
|
||||||
"ieee754": "^1.1.13"
|
"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": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
|
@ -777,6 +1105,21 @@
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
|
@ -1021,6 +1364,24 @@
|
||||||
"node": ">= 0.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": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
|
@ -1076,6 +1437,23 @@
|
||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
|
@ -1272,6 +1650,12 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -1313,6 +1697,12 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/jose": {
|
||||||
"version": "4.15.9",
|
"version": "4.15.9",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
|
@ -1621,6 +2011,18 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/mkdirp-classic": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
|
@ -1633,6 +2035,24 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/napi-build-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
|
@ -1660,6 +2080,26 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -1681,6 +2121,26 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
@ -1702,6 +2162,15 @@
|
||||||
"wrappy": "1"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
@ -1717,6 +2186,37 @@
|
||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
|
@ -1743,6 +2243,15 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -1834,6 +2343,68 @@
|
||||||
"node": ">= 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": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
|
@ -2059,6 +2630,14 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
|
@ -2077,6 +2656,23 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
|
@ -2105,6 +2701,30 @@
|
||||||
"node": ">=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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -2114,6 +2734,29 @@
|
||||||
"node": ">=0.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": {
|
"node_modules/tsx": {
|
||||||
"version": "4.21.0",
|
"version": "4.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
|
@ -2159,6 +2802,12 @@
|
||||||
"node": ">= 0.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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|
@ -2225,17 +2874,70 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"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": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"officeparser": "^6.0.4",
|
||||||
"uuid": "^11.0.5"
|
"uuid": "^11.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -23,6 +25,7 @@
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import cors from 'cors';
|
||||||
import { db } from './db/connection.js';
|
import { db } from './db/connection.js';
|
||||||
import quizzesRouter from './routes/quizzes.js';
|
import quizzesRouter from './routes/quizzes.js';
|
||||||
import usersRouter from './routes/users.js';
|
import usersRouter from './routes/users.js';
|
||||||
|
import uploadRouter from './routes/upload.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
@ -58,6 +59,7 @@ app.get('/health', (_req: Request, res: Response) => {
|
||||||
|
|
||||||
app.use('/api/quizzes', quizzesRouter);
|
app.use('/api/quizzes', quizzesRouter);
|
||||||
app.use('/api/users', usersRouter);
|
app.use('/api/users', usersRouter);
|
||||||
|
app.use('/api/upload', uploadRouter);
|
||||||
|
|
||||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
console.error('Unhandled error:', err);
|
console.error('Unhandled error:', err);
|
||||||
|
|
|
||||||
71
server/src/routes/upload.ts
Normal file
71
server/src/routes/upload.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
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;
|
||||||
123
server/src/services/documentParser.ts
Normal file
123
server/src/services/documentParser.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
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<string> {
|
||||||
|
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<ProcessedDocument> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { GoogleGenAI, Type } from "@google/genai";
|
import { GoogleGenAI, Type, createUserContent, createPartFromUri } from "@google/genai";
|
||||||
import { Quiz, Question, AnswerOption } from "../types";
|
import { Quiz, Question, AnswerOption, GenerateQuizOptions, ProcessedDocument } from "../types";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const getClient = () => {
|
const getClient = () => {
|
||||||
|
|
@ -10,61 +10,62 @@ const getClient = () => {
|
||||||
return new GoogleGenAI({ apiKey });
|
return new GoogleGenAI({ apiKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateQuiz = async (topic: string): Promise<Quiz> => {
|
const QUIZ_SCHEMA = {
|
||||||
const ai = getClient();
|
type: Type.OBJECT,
|
||||||
|
properties: {
|
||||||
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.`;
|
title: { type: Type.STRING, description: "A catchy title for the quiz" },
|
||||||
|
questions: {
|
||||||
const response = await ai.models.generateContent({
|
type: Type.ARRAY,
|
||||||
model: "gemini-3-flash-preview",
|
items: {
|
||||||
contents: prompt,
|
|
||||||
config: {
|
|
||||||
responseMimeType: "application/json",
|
|
||||||
responseSchema: {
|
|
||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: Type.STRING, description: "A catchy title for the quiz" },
|
text: { type: Type.STRING, description: "The question text" },
|
||||||
questions: {
|
options: {
|
||||||
type: Type.ARRAY,
|
type: Type.ARRAY,
|
||||||
items: {
|
items: {
|
||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
text: { type: Type.STRING, description: "The question text" },
|
text: { type: Type.STRING },
|
||||||
options: {
|
isCorrect: { type: Type.BOOLEAN },
|
||||||
type: Type.ARRAY,
|
reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" }
|
||||||
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", "options"]
|
required: ["text", "isCorrect", "reason"]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["title", "questions"]
|
required: ["text", "options"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
required: ["title", "questions"]
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.text) {
|
function buildPrompt(options: GenerateQuizOptions, hasDocuments: boolean): string {
|
||||||
throw new Error("Failed to generate quiz content");
|
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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(response.text);
|
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;
|
||||||
|
|
||||||
// Transform to our internal type with shapes/colors pre-assigned
|
|
||||||
const questions: Question[] = data.questions.map((q: any) => {
|
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) => ({
|
const options: AnswerOption[] = q.options.map((opt: any, index: number) => ({
|
||||||
text: opt.text,
|
text: opt.text,
|
||||||
isCorrect: opt.isCorrect,
|
isCorrect: opt.isCorrect,
|
||||||
|
|
@ -85,4 +86,67 @@ export const generateQuiz = async (topic: string): Promise<Quiz> => {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
questions
|
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<Quiz> => {
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
],
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": [
|
"types": [
|
||||||
"node"
|
"node",
|
||||||
|
"vite/client"
|
||||||
],
|
],
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|
|
||||||
12
types.ts
12
types.ts
|
|
@ -51,6 +51,18 @@ export interface QuizListItem {
|
||||||
updatedAt: string;
|
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 {
|
export interface Player {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue