import * as pdfjsLib from 'pdfjs-dist'; import mammoth from 'mammoth'; import { Question } from '../types'; // Configure a reliable CDN-hosted worker for PDF.js to prevent local asset bundle errors pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs`; export class DocumentParserService { /** * Primary Entrypoint: Extract raw string content from an uploaded File (PDF, DOCX, or TXT) */ static async extractTextFromFile(file: File): Promise { const extension = file.name.split('.').pop()?.toLowerCase(); if (extension === 'txt') { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(new Error("Unable to parse TXT document.")); reader.readAsText(file); }); } if (extension === 'docx') { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async () => { try { const arrayBuffer = reader.result as ArrayBuffer; const result = await mammoth.extractRawText({ arrayBuffer }); resolve(result.value); } catch (err: any) { reject(new Error("Unable to parse Microsoft Word Document (.docx): " + err.message)); } }; reader.onerror = () => reject(new Error("File load error in Word parser.")); reader.readAsArrayBuffer(file); }); } if (extension === 'pdf') { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async () => { try { const arrayBuffer = reader.result as ArrayBuffer; const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); const pdf = await loadingTask.promise; let extractedText = ""; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const pageText = textContent.items .map((item: any) => item.str) .join(" "); extractedText += pageText + "\n"; } resolve(extractedText); } catch (err: any) { reject(new Error("Unable to parse PDF document (.pdf): " + err.message)); } }; reader.onerror = () => reject(new Error("File load error in PDF parser.")); reader.readAsArrayBuffer(file); }); } throw new Error("Unsupported file schema! Please upload .TXT, .PDF, or .DOCX files."); } /** * Local Regex Parser Failsafe: Standard Regex pattern extraction in case the AI pipeline is offline */ static parseLocalRegex(text: string): Question[] { const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0); const questions: Question[] = []; let currentQuestion: Partial | null = null; lines.forEach((line) => { // Look for a question beginning with numbers: e.g. "1. What is..." or "Question 13:" const qMatch = line.match(/^(\d+)[\.\)]\s*(.*)$/i) || line.match(/^Question\s*(\d+)[:\.]?\s*(.*)$/i) || line.match(/^Q(\d+)[:\.]?\s*(.*)$/i); if (qMatch) { if (currentQuestion && currentQuestion.text) { questions.push(this.normalizeCleanQuestion(currentQuestion)); } currentQuestion = { id: `manual-regex-${Date.now()}-${qMatch[1]}`, text: qMatch[2], options: [], correctOptionId: 'opt-1', category: 'General', difficulty: 'medium', tags: [] }; return; } // Look for option formats: e.g. "A) Option" or "B. Other option" const oMatch = line.match(/^([A-D])[\.\)]\s*(.*)$/i) || line.match(/^\(([A-D])\)\s*(.*)$/i); if (oMatch && currentQuestion) { const letterIndex = oMatch[1].toUpperCase(); const optionIdMap: Record = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' }; currentQuestion.options?.push({ id: optionIdMap[letterIndex] || `opt-${letterIndex.toLowerCase()}`, text: oMatch[2] }); return; } // Look for declared answers: e.g. "Answer: B" or "ANS: C" const aMatch = line.match(/^(?:Answer|ANS)[:\s]+([A-D])/i); if (aMatch && currentQuestion) { const letterIndex = aMatch[1].toUpperCase(); const optionIdMap: Record = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' }; currentQuestion.correctOptionId = optionIdMap[letterIndex] || 'opt-1'; return; } // Append multi-line question text if appropriate if (currentQuestion && !oMatch && !aMatch) { currentQuestion.text += ' ' + line; } }); if (currentQuestion && currentQuestion.text) { questions.push(this.normalizeCleanQuestion(currentQuestion)); } return questions; } private static normalizeCleanQuestion(q: Partial): Question { const baseOptIds = ['opt-1', 'opt-2', 'opt-3', 'opt-4']; // Ensure accurate 4-option shape const options = q.options || []; while (options.length < 4) { options.push({ id: baseOptIds[options.length], text: `Option ${String.fromCharCode(65 + options.length)} placeholder` }); } const slicedOptions = options.slice(0, 4); return { text: q.text || "Untitled Question", options: slicedOptions, correctOptionId: q.correctOptionId || 'opt-1', category: q.category || 'General', difficulty: q.difficulty || 'medium', tags: q.tags || [] } as Question; } }