aki-008 commited on
Commit
1444e6f
·
1 Parent(s): f63667d

chore: fixed pdf parsing and quiz generation

Browse files
Backend/app/api/v1/endpoints/quiz.py CHANGED
@@ -12,7 +12,7 @@ import uuid
12
  import logging
13
 
14
 
15
- router = APIRouter(prefix="/quiz")
16
 
17
  logger = logging.getLogger("uvicorn.error")
18
 
 
12
  import logging
13
 
14
 
15
+ router = APIRouter()
16
 
17
  logger = logging.getLogger("uvicorn.error")
18
 
Backend/app/llm.py CHANGED
@@ -5,6 +5,7 @@ from typing import List, Optional, Any
5
  from app.schema.models import QuizOutput, QuizQuestion
6
  from app.config import settings
7
  from openai import AsyncOpenAI
 
8
 
9
  client = AsyncOpenAI(
10
  base_url="https://api.groq.com/openai/v1",
@@ -13,7 +14,7 @@ client = AsyncOpenAI(
13
 
14
  async def call_llm(prompt:str):
15
  try:
16
- response = client.chat.completions.create(
17
  # CRUCIAL: Use the LiteLLM format: 'gemini/gemini-2.5-pro'
18
  model="openai/gpt-oss-20b",
19
  messages=[
@@ -36,9 +37,6 @@ async def call_llm(prompt:str):
36
  raise e
37
 
38
 
39
-
40
- from typing import List
41
-
42
  async def stream_chat(messages: List[dict], context: str, retrieved_docs: str | None):
43
  system_instruction = {
44
  "role": "system",
 
5
  from app.schema.models import QuizOutput, QuizQuestion
6
  from app.config import settings
7
  from openai import AsyncOpenAI
8
+ from typing import List
9
 
10
  client = AsyncOpenAI(
11
  base_url="https://api.groq.com/openai/v1",
 
14
 
15
  async def call_llm(prompt:str):
16
  try:
17
+ response = await client.chat.completions.create(
18
  # CRUCIAL: Use the LiteLLM format: 'gemini/gemini-2.5-pro'
19
  model="openai/gpt-oss-20b",
20
  messages=[
 
37
  raise e
38
 
39
 
 
 
 
40
  async def stream_chat(messages: List[dict], context: str, retrieved_docs: str | None):
41
  system_instruction = {
42
  "role": "system",
Frontend/src/pages/quize.tsx CHANGED
@@ -1,6 +1,19 @@
1
- import React, { useState, useRef } from "react";
2
- import { FileSpreadsheet, Code2, ListChecks, FileText, Upload, X } from "lucide-react";
3
- import MCQQuizPage from "../components/quize/mcq";
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  const CodingQuizPage = ({ onBack }: any) => (
6
  <div className="min-h-screen bg-black text-white flex flex-col items-center justify-center">
@@ -21,16 +34,19 @@ const ResumeGeneratedQuize: React.FC = () => {
21
  const [uploadedFile, setUploadedFile] = useState<string | null>(null);
22
  const [fileError, setFileError] = useState<string | null>(null);
23
  const [fileObject, setFileObject] = useState<File | null>(null);
 
24
 
25
  const [customPrompt, setCustomPrompt] = useState("");
26
  const [quizData, setQuizData] = useState(null);
27
 
28
  const resumeInputRef = useRef<HTMLInputElement>(null);
29
  const notesInputRef = useRef<HTMLInputElement>(null);
30
-
31
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
32
 
33
- const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: "resume" | "notes") => {
 
 
 
34
  const file = event.target.files?.[0];
35
  if (!file) return;
36
 
@@ -40,14 +56,19 @@ const ResumeGeneratedQuize: React.FC = () => {
40
  }
41
 
42
  if (file.size > MAX_FILE_SIZE) {
43
- setFileError(`File size exceeds 10MB. Your file is ${(file.size / (1024 * 1024)).toFixed(2)}MB`);
 
 
 
 
 
44
  return;
45
  }
46
 
47
  setFileError(null);
48
  setUploadType(type);
49
  setUploadedFile(file.name);
50
- setFileObject(file); // <-- Store actual file object
51
  };
52
 
53
  const handleResumeClick = () => resumeInputRef.current?.click();
@@ -60,38 +81,102 @@ const ResumeGeneratedQuize: React.FC = () => {
60
  setFileObject(null);
61
  };
62
 
63
- // ---------- SEND PDF TO BACKEND ----------
64
- const generateQuiz = async () => {
65
- if (!fileObject || !quizType) return;
 
 
 
 
 
 
 
 
 
66
 
67
- const formData = new FormData();
68
- formData.append("file", fileObject);
69
- formData.append("quiz_type", quizType);
70
- formData.append("instructions", customPrompt);
 
71
 
72
- const response = await fetch("http://localhost:8000/generate-quiz", {
73
- method: "POST",
74
- body: formData,
75
- });
76
 
77
- const data = await response.json();
 
78
 
79
- setQuizData(data); // store quiz data
80
- setShowQuiz(true); // navigate to quiz page
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  };
82
 
 
 
83
  // Load quiz UI
84
  if (showQuiz) {
85
  if (quizType === "mcq")
86
  return <MCQQuizPage data={quizData} onBack={() => setShowQuiz(false)} />;
87
-
88
  if (quizType === "coding")
89
- return <CodingQuizPage data={quizData} onBack={() => setShowQuiz(false)} />;
 
 
90
  }
91
 
92
- // ---- UI COMPONENTS (same as your original) ----
93
  const buttonClass =
94
- "w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-400 hover:to-blue-600 font-medium transition shadow-lg shadow-blue-500/30 text-white mt-4";
95
 
96
  const OutputTypeOption = ({ icon, title, desc, value }: any) => (
97
  <div
@@ -118,10 +203,12 @@ const ResumeGeneratedQuize: React.FC = () => {
118
  <FileSpreadsheet className="w-8 h-8 text-cyan-400" />
119
  <div>
120
  <h2 className="text-2xl font-bold">Upload Materials</h2>
121
- <p className="text-gray-400 mt-1 text-sm">Upload your resume or notes (Max 10MB)</p>
 
 
122
  </div>
123
  </div>
124
-
125
  <input
126
  ref={resumeInputRef}
127
  type="file"
@@ -163,9 +250,13 @@ const ResumeGeneratedQuize: React.FC = () => {
163
  {uploadedFile && (
164
  <div className="mt-4 p-3 bg-green-500/20 border border-green-500/50 rounded-lg flex items-center justify-between">
165
  <div>
166
- <p className="text-sm text-green-300 font-medium">File uploaded successfully</p>
 
 
167
  <p className="text-xs text-green-200 mt-1">{uploadedFile}</p>
168
- <p className="text-xs text-green-200">{uploadType === "resume" ? "Resume" : "Notes"}</p>
 
 
169
  </div>
170
  <button
171
  onClick={clearUpload}
@@ -240,15 +331,20 @@ const ResumeGeneratedQuize: React.FC = () => {
240
  <button
241
  className={
242
  buttonClass +
243
- " text-xl mt-8 " +
244
- (!quizType || !fileObject
245
- ? "opacity-50 cursor-not-allowed"
246
- : "opacity-100 cursor-pointer")
247
  }
248
- disabled={!quizType || !fileObject}
249
- onClick={generateQuiz} // <--- SEND PDF TO BACKEND
250
  >
251
- Generate Quiz Now
 
 
 
 
 
 
252
  </button>
253
  </div>
254
  </div>
 
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import {
3
+ FileSpreadsheet,
4
+ Code2,
5
+ ListChecks,
6
+ Upload,
7
+ X,
8
+ Loader2,
9
+ } from "lucide-react";
10
+ import MCQQuizPage from "../components/quize/mcq";
11
+ import API from "../api/api"; // Import your Axios instance
12
+ import * as pdfjsLib from "pdfjs-dist";
13
+
14
+ // Initialize PDF worker
15
+ // In a Vite setup, pointing to a CDN is often the most stable way to avoid build config issues
16
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
17
 
18
  const CodingQuizPage = ({ onBack }: any) => (
19
  <div className="min-h-screen bg-black text-white flex flex-col items-center justify-center">
 
34
  const [uploadedFile, setUploadedFile] = useState<string | null>(null);
35
  const [fileError, setFileError] = useState<string | null>(null);
36
  const [fileObject, setFileObject] = useState<File | null>(null);
37
+ const [isProcessing, setIsProcessing] = useState(false); // Loading state
38
 
39
  const [customPrompt, setCustomPrompt] = useState("");
40
  const [quizData, setQuizData] = useState(null);
41
 
42
  const resumeInputRef = useRef<HTMLInputElement>(null);
43
  const notesInputRef = useRef<HTMLInputElement>(null);
 
44
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
45
 
46
+ const handleFileUpload = (
47
+ event: React.ChangeEvent<HTMLInputElement>,
48
+ type: "resume" | "notes"
49
+ ) => {
50
  const file = event.target.files?.[0];
51
  if (!file) return;
52
 
 
56
  }
57
 
58
  if (file.size > MAX_FILE_SIZE) {
59
+ setFileError(
60
+ `File size exceeds 10MB. Your file is ${(
61
+ file.size /
62
+ (1024 * 1024)
63
+ ).toFixed(2)}MB`
64
+ );
65
  return;
66
  }
67
 
68
  setFileError(null);
69
  setUploadType(type);
70
  setUploadedFile(file.name);
71
+ setFileObject(file);
72
  };
73
 
74
  const handleResumeClick = () => resumeInputRef.current?.click();
 
81
  setFileObject(null);
82
  };
83
 
84
+ // --- PDF TEXT EXTRACTION ---
85
+ const extractTextFromPDF = async (file: File): Promise<string> => {
86
+ const arrayBuffer = await file.arrayBuffer();
87
+ const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
88
+ let fullText = "";
89
+
90
+ for (let i = 1; i <= pdf.numPages; i++) {
91
+ const page = await pdf.getPage(i);
92
+ const textContent = await page.getTextContent();
93
+ const pageText = textContent.items.map((item: any) => item.str).join(" ");
94
+ fullText += pageText + "\n";
95
+ }
96
 
97
+ return fullText;
98
+ };
99
+
100
+ // ---------- GENERATE QUIZ ----------
101
+ // --- START: Replace your existing generateQuiz function ---
102
 
103
+ const generateQuiz = async () => {
104
+ if (!fileObject || !quizType) return;
 
 
105
 
106
+ setIsProcessing(true);
107
+ setFileError(null);
108
 
109
+ try {
110
+ // 1. Extract text on the client side
111
+ const extractedText = await extractTextFromPDF(fileObject);
112
+
113
+ if (!extractedText.trim()) {
114
+ throw new Error(
115
+ "Could not extract text from the PDF. It might be an image-only PDF."
116
+ );
117
+ }
118
+
119
+ // 2. Prepare Payload matching Backend `Quiz_input` schema
120
+ const payload = {
121
+ // Ensure extractedText is cleaned up to prevent large string issues
122
+ parsed_doc: extractedText.trim(),
123
+ // Ensure user_prompt is always a valid string
124
+ user_prompt:
125
+ customPrompt.trim() || "Generate a quiz based on this content.",
126
+ };
127
+
128
+ // 3. Send to Backend
129
+ // The URL is now correct: /api/v1/quiz/resume
130
+ const response = await API.post("/quiz/resume", payload);
131
+
132
+ // Success
133
+ setQuizData(response.data);
134
+ setShowQuiz(true);
135
+ } catch (error: any) {
136
+ console.error("Quiz Generation Error:", error);
137
+
138
+ // Extract detailed error message from FastAPI response body
139
+ let errorMessage = "Failed to generate quiz. Check login status.";
140
+ if (error.response?.data?.detail) {
141
+ // Check if the server returned a validation error list or a simple string
142
+ if (typeof error.response.data.detail === "string") {
143
+ errorMessage = error.response.data.detail;
144
+ } else if (
145
+ Array.isArray(error.response.data.detail) &&
146
+ error.response.data.detail.length > 0
147
+ ) {
148
+ // Pydantic validation error format
149
+ const firstError = error.response.data.detail[0];
150
+ errorMessage = `Validation Error: Field '${firstError.loc.join(
151
+ " -> "
152
+ )}' ${firstError.msg}`;
153
+ }
154
+ } else if (error.code === "ERR_BAD_REQUEST") {
155
+ // General network/Axios 400 error
156
+ errorMessage = "Server rejected the data. Are you logged in?";
157
+ }
158
+
159
+ setFileError(errorMessage);
160
+ } finally {
161
+ setIsProcessing(false);
162
+ }
163
  };
164
 
165
+ // --- END: Replace your existing generateQuiz function ---
166
+
167
  // Load quiz UI
168
  if (showQuiz) {
169
  if (quizType === "mcq")
170
  return <MCQQuizPage data={quizData} onBack={() => setShowQuiz(false)} />;
 
171
  if (quizType === "coding")
172
+ return (
173
+ <CodingQuizPage data={quizData} onBack={() => setShowQuiz(false)} />
174
+ );
175
  }
176
 
177
+ // ---- UI COMPONENTS ----
178
  const buttonClass =
179
+ "w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-400 hover:to-blue-600 font-medium transition shadow-lg shadow-blue-500/30 text-white mt-4 flex justify-center items-center gap-2";
180
 
181
  const OutputTypeOption = ({ icon, title, desc, value }: any) => (
182
  <div
 
203
  <FileSpreadsheet className="w-8 h-8 text-cyan-400" />
204
  <div>
205
  <h2 className="text-2xl font-bold">Upload Materials</h2>
206
+ <p className="text-gray-400 mt-1 text-sm">
207
+ Upload your resume or notes (Max 10MB)
208
+ </p>
209
  </div>
210
  </div>
211
+
212
  <input
213
  ref={resumeInputRef}
214
  type="file"
 
250
  {uploadedFile && (
251
  <div className="mt-4 p-3 bg-green-500/20 border border-green-500/50 rounded-lg flex items-center justify-between">
252
  <div>
253
+ <p className="text-sm text-green-300 font-medium">
254
+ File uploaded successfully
255
+ </p>
256
  <p className="text-xs text-green-200 mt-1">{uploadedFile}</p>
257
+ <p className="text-xs text-green-200">
258
+ {uploadType === "resume" ? "Resume" : "Notes"}
259
+ </p>
260
  </div>
261
  <button
262
  onClick={clearUpload}
 
331
  <button
332
  className={
333
  buttonClass +
334
+ (!quizType || !fileObject || isProcessing
335
+ ? " opacity-50 cursor-not-allowed"
336
+ : " opacity-100 cursor-pointer")
 
337
  }
338
+ disabled={!quizType || !fileObject || isProcessing}
339
+ onClick={generateQuiz}
340
  >
341
+ {isProcessing ? (
342
+ <>
343
+ <Loader2 className="animate-spin w-5 h-5" /> Generating...
344
+ </>
345
+ ) : (
346
+ "Generate Quiz Now"
347
+ )}
348
  </button>
349
  </div>
350
  </div>