File size: 5,800 Bytes
c04a174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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<string> {
    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<Question> | 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<string, string> = { 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<string, string> = { 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>): 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;
  }
}