LiamKhoaLe commited on
Commit
704792a
·
1 Parent(s): 7a1ebee

Init quiz mode

Browse files
Files changed (9) hide show
  1. .DS_Store +0 -0
  2. app.py +1 -0
  3. helpers/models.py +27 -0
  4. routes/quiz.py +490 -0
  5. static/index.html +101 -0
  6. static/quiz.js +546 -0
  7. static/script.js +12 -0
  8. static/styles.css +334 -0
  9. utils/rag/rag.py +23 -0
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
app.py CHANGED
@@ -12,6 +12,7 @@ import routes.chats as _routes_chat
12
  import routes.sessions as _routes_sessions
13
  import routes.health as _routes_health
14
  import routes.analytics as _routes_analytics
 
15
 
16
  # Local dev
17
  # if __name__ == "__main__":
 
12
  import routes.sessions as _routes_sessions
13
  import routes.health as _routes_health
14
  import routes.analytics as _routes_analytics
15
+ import routes.quiz as _routes_quiz
16
 
17
  # Local dev
18
  # if __name__ == "__main__":
helpers/models.py CHANGED
@@ -30,6 +30,8 @@ class ChatHistoryResponse(BaseModel):
30
 
31
  class MessageResponse(BaseModel):
32
  message: str
 
 
33
 
34
  class UploadResponse(BaseModel):
35
  job_id: str
@@ -60,4 +62,29 @@ class StatusUpdateResponse(BaseModel):
60
  message: str
61
  progress: Optional[int] = None
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
 
30
 
31
  class MessageResponse(BaseModel):
32
  message: str
33
+ quiz: Optional[Dict[str, Any]] = None
34
+ results: Optional[Dict[str, Any]] = None
35
 
36
  class UploadResponse(BaseModel):
37
  job_id: str
 
62
  message: str
63
  progress: Optional[int] = None
64
 
65
+ class QuizQuestionResponse(BaseModel):
66
+ type: str # "mcq" or "self_reflect"
67
+ question: str
68
+ options: Optional[List[str]] = None # For MCQ questions
69
+ correct_answer: Optional[int] = None # For MCQ questions
70
+ topic: str
71
+ complexity: str
72
+
73
+ class QuizResponse(BaseModel):
74
+ quiz_id: str
75
+ user_id: str
76
+ project_id: str
77
+ questions: List[QuizQuestionResponse]
78
+ time_limit: int
79
+ documents: List[str]
80
+ created_at: str
81
+ status: str
82
+
83
+ class QuizResultResponse(BaseModel):
84
+ questions: List[Dict[str, Any]]
85
+ total_score: float
86
+ correct_count: int
87
+ partial_count: int
88
+ incorrect_count: int
89
+
90
 
routes/quiz.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # routes/quiz.py
2
+ import json, time, uuid, asyncio
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Dict, List, Optional
5
+ from fastapi import Form, HTTPException
6
+
7
+ from helpers.setup import app, rag, logger, nvidia_rotator, gemini_rotator
8
+ from helpers.models import MessageResponse, QuizResponse, QuizResultResponse
9
+ from utils.api.router import select_model, generate_answer_with_model, NVIDIA_SMALL, NVIDIA_MEDIUM, NVIDIA_LARGE
10
+ from utils.analytics import get_analytics_tracker
11
+
12
+
13
+ @app.post("/quiz/create", response_model=MessageResponse)
14
+ async def create_quiz(
15
+ user_id: str = Form(...),
16
+ project_id: str = Form(...),
17
+ questions_input: str = Form(...),
18
+ time_limit: str = Form(...),
19
+ documents: str = Form(...)
20
+ ):
21
+ """Create a quiz from selected documents"""
22
+ try:
23
+ # Parse documents
24
+ selected_docs = json.loads(documents)
25
+ time_limit_minutes = int(time_limit)
26
+
27
+ logger.info(f"[QUIZ] Creating quiz for user {user_id}, project {project_id}")
28
+ logger.info(f"[QUIZ] Documents: {selected_docs}")
29
+ logger.info(f"[QUIZ] Questions input: {questions_input}")
30
+ logger.info(f"[QUIZ] Time limit: {time_limit_minutes} minutes")
31
+
32
+ # Step 1: Parse user input to determine question counts
33
+ question_config = await parse_question_input(questions_input, nvidia_rotator)
34
+ logger.info(f"[QUIZ] Parsed question config: {question_config}")
35
+
36
+ # Step 2: Get document summaries and key topics
37
+ document_summaries = await get_document_summaries(user_id, project_id, selected_docs)
38
+ key_topics = await extract_key_topics(document_summaries, nvidia_rotator)
39
+ logger.info(f"[QUIZ] Extracted {len(key_topics)} key topics")
40
+
41
+ # Step 3: Create question generation plan
42
+ generation_plan = await create_question_plan(
43
+ question_config, key_topics, nvidia_rotator
44
+ )
45
+ logger.info(f"[QUIZ] Created generation plan with {len(generation_plan)} tasks")
46
+
47
+ # Step 4: Generate questions and answers
48
+ questions = await generate_questions_and_answers(
49
+ generation_plan, document_summaries, nvidia_rotator
50
+ )
51
+ logger.info(f"[QUIZ] Generated {len(questions)} questions")
52
+
53
+ # Step 5: Create quiz record
54
+ quiz_id = str(uuid.uuid4())
55
+ quiz_data = {
56
+ "quiz_id": quiz_id,
57
+ "user_id": user_id,
58
+ "project_id": project_id,
59
+ "questions": questions,
60
+ "time_limit": time_limit_minutes,
61
+ "documents": selected_docs,
62
+ "created_at": datetime.now(timezone.utc),
63
+ "status": "ready"
64
+ }
65
+
66
+ # Store quiz in database
67
+ rag.db["quizzes"].insert_one(quiz_data)
68
+
69
+ return MessageResponse(message="Quiz created successfully", quiz=quiz_data)
70
+
71
+ except Exception as e:
72
+ logger.error(f"[QUIZ] Quiz creation failed: {e}")
73
+ raise HTTPException(500, detail=f"Failed to create quiz: {str(e)}")
74
+
75
+
76
+ @app.post("/quiz/submit", response_model=MessageResponse)
77
+ async def submit_quiz(
78
+ user_id: str = Form(...),
79
+ project_id: str = Form(...),
80
+ quiz_id: str = Form(...),
81
+ answers: str = Form(...)
82
+ ):
83
+ """Submit quiz answers and get results"""
84
+ try:
85
+ # Parse answers
86
+ user_answers = json.loads(answers)
87
+
88
+ # Get quiz data
89
+ quiz = rag.db["quizzes"].find_one({
90
+ "quiz_id": quiz_id,
91
+ "user_id": user_id,
92
+ "project_id": project_id
93
+ })
94
+
95
+ if not quiz:
96
+ raise HTTPException(404, detail="Quiz not found")
97
+
98
+ # Mark answers
99
+ results = await mark_quiz_answers(quiz["questions"], user_answers, nvidia_rotator)
100
+
101
+ # Store results
102
+ result_data = {
103
+ "quiz_id": quiz_id,
104
+ "user_id": user_id,
105
+ "project_id": project_id,
106
+ "answers": user_answers,
107
+ "results": results,
108
+ "submitted_at": datetime.now(timezone.utc)
109
+ }
110
+
111
+ rag.db["quiz_results"].insert_one(result_data)
112
+
113
+ return MessageResponse(message="Quiz submitted successfully", results=results)
114
+
115
+ except Exception as e:
116
+ logger.error(f"[QUIZ] Quiz submission failed: {e}")
117
+ raise HTTPException(500, detail=f"Failed to submit quiz: {str(e)}")
118
+
119
+
120
+ async def parse_question_input(questions_input: str, nvidia_rotator) -> Dict[str, int]:
121
+ """Parse user input to determine MCQ and self-reflect question counts"""
122
+ system_prompt = """You are an expert at parsing user requests for quiz questions.
123
+ Given a user's input about how many questions they want, extract the number of MCQ and self-reflect questions.
124
+
125
+ Return ONLY a JSON object with this exact format:
126
+ {
127
+ "mcq": <number of multiple choice questions>,
128
+ "sr": <number of self-reflect questions>
129
+ }
130
+
131
+ If the user doesn't specify types, assume they want MCQ questions.
132
+ If they say "total" or "questions", split them roughly 70% MCQ and 30% self-reflect.
133
+ """
134
+
135
+ user_prompt = f"User input: {questions_input}\n\nExtract the question counts:"
136
+
137
+ try:
138
+ # Use NVIDIA_SMALL for parsing
139
+ response = await generate_answer_with_model(
140
+ selection={"provider": "nvidia", "model": NVIDIA_SMALL},
141
+ system_prompt=system_prompt,
142
+ user_prompt=user_prompt,
143
+ gemini_rotator=gemini_rotator,
144
+ nvidia_rotator=nvidia_rotator,
145
+ user_id="system",
146
+ context="quiz_parsing"
147
+ )
148
+
149
+ # Parse JSON response
150
+ config = json.loads(response.strip())
151
+ return {
152
+ "mcq": int(config.get("mcq", 0)),
153
+ "sr": int(config.get("sr", 0))
154
+ }
155
+
156
+ except Exception as e:
157
+ logger.warning(f"[QUIZ] Failed to parse question input: {e}")
158
+ # Fallback: assume 10 MCQ questions
159
+ return {"mcq": 10, "sr": 0}
160
+
161
+
162
+ async def get_document_summaries(user_id: str, project_id: str, documents: List[str]) -> str:
163
+ """Get summaries from selected documents"""
164
+ summaries = []
165
+
166
+ for doc in documents:
167
+ try:
168
+ # Get file summary
169
+ file_data = rag.get_file_summary(user_id=user_id, project_id=project_id, filename=doc)
170
+ if file_data and file_data.get("summary"):
171
+ summaries.append(f"[{doc}] {file_data['summary']}")
172
+
173
+ # Get additional chunks for more context
174
+ chunks = rag.get_file_chunks(user_id=user_id, project_id=project_id, filename=doc, limit=20)
175
+ if chunks:
176
+ chunk_text = "\n".join([chunk.get("content", "") for chunk in chunks[:10]])
177
+ summaries.append(f"[{doc} - Additional Content] {chunk_text[:2000]}...")
178
+
179
+ except Exception as e:
180
+ logger.warning(f"[QUIZ] Failed to get summary for {doc}: {e}")
181
+ continue
182
+
183
+ return "\n\n".join(summaries)
184
+
185
+
186
+ async def extract_key_topics(document_summaries: str, nvidia_rotator) -> List[str]:
187
+ """Extract key topics from document summaries"""
188
+ system_prompt = """You are an expert at analyzing educational content and extracting key topics.
189
+ Given document summaries, identify the main topics and concepts that would be suitable for quiz questions.
190
+
191
+ Return a JSON array of topic strings, focusing on:
192
+ - Main concepts and theories
193
+ - Important facts and details
194
+ - Key processes and procedures
195
+ - Critical thinking points
196
+
197
+ Limit to 10-15 most important topics.
198
+ """
199
+
200
+ user_prompt = f"Document summaries:\n{document_summaries}\n\nExtract key topics:"
201
+
202
+ try:
203
+ # Use NVIDIA_MEDIUM for topic extraction
204
+ response = await generate_answer_with_model(
205
+ selection={"provider": "qwen", "model": NVIDIA_MEDIUM},
206
+ system_prompt=system_prompt,
207
+ user_prompt=user_prompt,
208
+ gemini_rotator=gemini_rotator,
209
+ nvidia_rotator=nvidia_rotator,
210
+ user_id="system",
211
+ context="quiz_topic_extraction"
212
+ )
213
+
214
+ topics = json.loads(response.strip())
215
+ return topics if isinstance(topics, list) else []
216
+
217
+ except Exception as e:
218
+ logger.warning(f"[QUIZ] Failed to extract topics: {e}")
219
+ return ["General Knowledge", "Key Concepts", "Important Details"]
220
+
221
+
222
+ async def create_question_plan(question_config: Dict, key_topics: List[str], nvidia_rotator) -> List[Dict]:
223
+ """Create a plan for question generation"""
224
+ system_prompt = """You are an expert at creating quiz question generation plans.
225
+ Given question counts and topics, create a detailed plan for generating questions.
226
+
227
+ Return a JSON array of task objects, each with:
228
+ - description: what type of questions to generate
229
+ - complexity: "high", "medium", or "low"
230
+ - topic: which topic to focus on
231
+ - number_mcq: number of MCQ questions for this task
232
+ - number_sr: number of self-reflect questions for this task
233
+
234
+ Distribute questions across topics and complexity levels.
235
+ """
236
+
237
+ user_prompt = f"""Question config: {question_config}
238
+ Key topics: {key_topics}
239
+
240
+ Create a generation plan:"""
241
+
242
+ try:
243
+ # Use NVIDIA_MEDIUM for planning
244
+ response = await generate_answer_with_model(
245
+ selection={"provider": "qwen", "model": NVIDIA_MEDIUM},
246
+ system_prompt=system_prompt,
247
+ user_prompt=user_prompt,
248
+ gemini_rotator=gemini_rotator,
249
+ nvidia_rotator=nvidia_rotator,
250
+ user_id="system",
251
+ context="quiz_planning"
252
+ )
253
+
254
+ plan = json.loads(response.strip())
255
+ return plan if isinstance(plan, list) else []
256
+
257
+ except Exception as e:
258
+ logger.warning(f"[QUIZ] Failed to create plan: {e}")
259
+ # Fallback plan
260
+ return [{
261
+ "description": "Generate general questions",
262
+ "complexity": "medium",
263
+ "topic": "General",
264
+ "number_mcq": question_config.get("mcq", 0),
265
+ "number_sr": question_config.get("sr", 0)
266
+ }]
267
+
268
+
269
+ async def generate_questions_and_answers(plan: List[Dict], document_summaries: str, nvidia_rotator) -> List[Dict]:
270
+ """Generate questions and answers based on the plan"""
271
+ all_questions = []
272
+
273
+ for task in plan:
274
+ try:
275
+ # Generate questions for this task
276
+ questions = await generate_task_questions(task, document_summaries, nvidia_rotator)
277
+ all_questions.extend(questions)
278
+
279
+ except Exception as e:
280
+ logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
281
+ continue
282
+
283
+ return all_questions
284
+
285
+
286
+ async def generate_task_questions(task: Dict, document_summaries: str, nvidia_rotator) -> List[Dict]:
287
+ """Generate questions for a specific task"""
288
+ system_prompt = f"""You are an expert quiz question generator.
289
+ Generate {task.get('number_mcq', 0)} multiple choice questions and {task.get('number_sr', 0)} self-reflection questions.
290
+
291
+ For MCQ questions:
292
+ - Create clear, well-structured questions
293
+ - Provide 4 answer options (A, B, C, D)
294
+ - Mark the correct answer
295
+ - Make distractors plausible but incorrect
296
+
297
+ For self-reflection questions:
298
+ - Create open-ended questions that require critical thinking
299
+ - Focus on analysis, evaluation, and synthesis
300
+ - Encourage personal reflection and application
301
+
302
+ Return a JSON array of question objects with this format:
303
+ {{
304
+ "type": "mcq" or "self_reflect",
305
+ "question": "question text",
306
+ "options": ["option1", "option2", "option3", "option4"] (for MCQ only),
307
+ "correct_answer": 0 (index for MCQ, null for self_reflect),
308
+ "topic": "topic name",
309
+ "complexity": "high/medium/low"
310
+ }}
311
+ """
312
+
313
+ user_prompt = f"""Task: {task}
314
+ Document content: {document_summaries[:3000]}...
315
+
316
+ Generate questions:"""
317
+
318
+ try:
319
+ # Use appropriate model based on complexity
320
+ if task.get("complexity") == "high":
321
+ model_selection = {"provider": "nvidia_large", "model": NVIDIA_LARGE}
322
+ else:
323
+ model_selection = {"provider": "nvidia", "model": NVIDIA_SMALL}
324
+
325
+ response = await generate_answer_with_model(
326
+ selection=model_selection,
327
+ system_prompt=system_prompt,
328
+ user_prompt=user_prompt,
329
+ gemini_rotator=gemini_rotator,
330
+ nvidia_rotator=nvidia_rotator,
331
+ user_id="system",
332
+ context="quiz_question_generation"
333
+ )
334
+
335
+ questions = json.loads(response.strip())
336
+ return questions if isinstance(questions, list) else []
337
+
338
+ except Exception as e:
339
+ logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
340
+ return []
341
+
342
+
343
+ async def mark_quiz_answers(questions: List[Dict], user_answers: Dict, nvidia_rotator) -> Dict:
344
+ """Mark quiz answers and provide feedback"""
345
+ results = {
346
+ "questions": [],
347
+ "total_score": 0,
348
+ "correct_count": 0,
349
+ "partial_count": 0,
350
+ "incorrect_count": 0
351
+ }
352
+
353
+ for i, question in enumerate(questions):
354
+ user_answer = user_answers.get(str(i))
355
+ question_result = await mark_single_question(question, user_answer, nvidia_rotator)
356
+ results["questions"].append(question_result)
357
+
358
+ # Update counts
359
+ if question_result["status"] == "correct":
360
+ results["correct_count"] += 1
361
+ results["total_score"] += 1
362
+ elif question_result["status"] == "partial":
363
+ results["partial_count"] += 1
364
+ results["total_score"] += 0.5
365
+ else:
366
+ results["incorrect_count"] += 1
367
+
368
+ return results
369
+
370
+
371
+ async def mark_single_question(question: Dict, user_answer: Any, nvidia_rotator) -> Dict:
372
+ """Mark a single question"""
373
+ result = {
374
+ "question": question["question"],
375
+ "type": question["type"],
376
+ "user_answer": user_answer,
377
+ "status": "incorrect",
378
+ "explanation": ""
379
+ }
380
+
381
+ if question["type"] == "mcq":
382
+ # MCQ marking
383
+ correct_index = question.get("correct_answer", 0)
384
+ if user_answer is not None and int(user_answer) == correct_index:
385
+ result["status"] = "correct"
386
+ result["explanation"] = "Correct answer!"
387
+ else:
388
+ result["status"] = "incorrect"
389
+ result["explanation"] = await generate_mcq_explanation(question, user_answer, nvidia_rotator)
390
+
391
+ result["correct_answer"] = correct_index
392
+ result["options"] = question.get("options", [])
393
+
394
+ elif question["type"] == "self_reflect":
395
+ # Self-reflection marking using AI
396
+ result["status"] = await evaluate_self_reflect_answer(question, user_answer, nvidia_rotator)
397
+ result["explanation"] = await generate_self_reflect_feedback(question, user_answer, result["status"], nvidia_rotator)
398
+
399
+ return result
400
+
401
+
402
+ async def generate_mcq_explanation(question: Dict, user_answer: Any, nvidia_rotator) -> str:
403
+ """Generate explanation for MCQ answer"""
404
+ system_prompt = """You are an expert tutor. Explain why the user's answer was wrong and why the correct answer is right.
405
+ Be concise but helpful. Focus on the key concept being tested."""
406
+
407
+ user_prompt = f"""Question: {question['question']}
408
+ Options: {question.get('options', [])}
409
+ User's answer: {user_answer}
410
+ Correct answer: {question.get('correct_answer', 0)}
411
+
412
+ Explain why the user was wrong:"""
413
+
414
+ try:
415
+ response = await generate_answer_with_model(
416
+ selection={"provider": "nvidia", "model": NVIDIA_SMALL},
417
+ system_prompt=system_prompt,
418
+ user_prompt=user_prompt,
419
+ gemini_rotator=gemini_rotator,
420
+ nvidia_rotator=nvidia_rotator,
421
+ user_id="system",
422
+ context="quiz_explanation"
423
+ )
424
+ return response
425
+ except Exception as e:
426
+ logger.warning(f"[QUIZ] Failed to generate MCQ explanation: {e}")
427
+ return "The correct answer is different from your choice. Please review the material."
428
+
429
+
430
+ async def evaluate_self_reflect_answer(question: Dict, user_answer: str, nvidia_rotator) -> str:
431
+ """Evaluate self-reflection answer using AI"""
432
+ system_prompt = """You are an expert educator evaluating student responses.
433
+ Evaluate the student's answer and determine if it's correct, partially correct, or incorrect.
434
+
435
+ Return only one word: "correct", "partial", or "incorrect"
436
+ """
437
+
438
+ user_prompt = f"""Question: {question['question']}
439
+ Student's answer: {user_answer or 'No answer provided'}
440
+
441
+ Evaluate the answer:"""
442
+
443
+ try:
444
+ response = await generate_answer_with_model(
445
+ selection={"provider": "nvidia", "model": NVIDIA_SMALL},
446
+ system_prompt=system_prompt,
447
+ user_prompt=user_prompt,
448
+ gemini_rotator=gemini_rotator,
449
+ nvidia_rotator=nvidia_rotator,
450
+ user_id="system",
451
+ context="quiz_evaluation"
452
+ )
453
+
454
+ response = response.strip().lower()
455
+ if response in ["correct", "partial", "incorrect"]:
456
+ return response
457
+ else:
458
+ return "partial" # Default to partial if unclear
459
+
460
+ except Exception as e:
461
+ logger.warning(f"[QUIZ] Failed to evaluate self-reflect answer: {e}")
462
+ return "partial"
463
+
464
+
465
+ async def generate_self_reflect_feedback(question: Dict, user_answer: str, status: str, nvidia_rotator) -> str:
466
+ """Generate feedback for self-reflection answer"""
467
+ system_prompt = """You are an expert tutor providing feedback on student responses.
468
+ Provide constructive feedback that helps the student understand their answer and improve.
469
+ Be encouraging but honest about areas for improvement."""
470
+
471
+ user_prompt = f"""Question: {question['question']}
472
+ Student's answer: {user_answer or 'No answer provided'}
473
+ Evaluation: {status}
474
+
475
+ Provide feedback:"""
476
+
477
+ try:
478
+ response = await generate_answer_with_model(
479
+ selection={"provider": "nvidia", "model": NVIDIA_SMALL},
480
+ system_prompt=system_prompt,
481
+ user_prompt=user_prompt,
482
+ gemini_rotator=gemini_rotator,
483
+ nvidia_rotator=nvidia_rotator,
484
+ user_id="system",
485
+ context="quiz_feedback"
486
+ )
487
+ return response
488
+ except Exception as e:
489
+ logger.warning(f"[QUIZ] Failed to generate self-reflect feedback: {e}")
490
+ return "Thank you for your response. Please review the material for a more complete answer."
static/index.html CHANGED
@@ -319,6 +319,13 @@
319
  </svg>
320
  <span>Search</span>
321
  </button>
 
 
 
 
 
 
 
322
  </div>
323
  </div>
324
  <div class="chat-hint" id="chat-hint">
@@ -433,6 +440,99 @@
433
  </div>
434
  </div>
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  <!-- Loading Overlay -->
437
  <div id="loading-overlay" class="loading-overlay hidden">
438
  <div class="loading-content">
@@ -451,5 +551,6 @@
451
  <script src="/static/script.js"></script>
452
  <script src="/static/sessions.js"></script>
453
  <script src="/static/analytics.js"></script>
 
454
  </body>
455
  </html>
 
319
  </svg>
320
  <span>Search</span>
321
  </button>
322
+ <button id="quiz-link" class="action-pill" title="Create quiz from selected documents">
323
+ <svg viewBox="0 0 24 24" aria-hidden="true">
324
+ <path d="M9 12l2 2 4-4"></path>
325
+ <path d="M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9 9 4.03 9 9z"></path>
326
+ </svg>
327
+ <span>Quiz</span>
328
+ </button>
329
  </div>
330
  </div>
331
  <div class="chat-hint" id="chat-hint">
 
440
  </div>
441
  </div>
442
 
443
+ <!-- Quiz Setup Modal -->
444
+ <div id="quiz-setup-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-setup-title">
445
+ <div class="modal-content">
446
+ <div class="modal-header">
447
+ <h2 id="quiz-setup-title">Create Quiz</h2>
448
+ <p class="modal-subtitle">Configure your quiz settings</p>
449
+ </div>
450
+ <form id="quiz-setup-form">
451
+ <!-- Step 1: Question Configuration -->
452
+ <div class="quiz-step" id="quiz-step-1">
453
+ <div class="form-group">
454
+ <label>How many questions would you like?</label>
455
+ <textarea id="quiz-questions-input" placeholder="Tell me how many MCQ and self-reflect questions you want. For example: 'I want 25 multiple choice questions and 10 self-reflection questions' or 'Give me 30 questions total, split between MCQ and self-reflect questions'" rows="3" required></textarea>
456
+ </div>
457
+ </div>
458
+
459
+ <!-- Step 2: Time Limit -->
460
+ <div class="quiz-step" id="quiz-step-2" style="display:none;">
461
+ <div class="form-group">
462
+ <label>Time Limit (minutes)</label>
463
+ <input type="number" id="quiz-time-limit" placeholder="Enter time limit in minutes (0 for no limit)" min="0" value="0">
464
+ <small>Enter 0 for no time limit</small>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- Step 3: Document Selection -->
469
+ <div class="quiz-step" id="quiz-step-3" style="display:none;">
470
+ <div class="form-group">
471
+ <label>Select Documents</label>
472
+ <div id="quiz-document-list" class="document-checkbox-list">
473
+ <!-- Documents will be populated here -->
474
+ </div>
475
+ </div>
476
+ </div>
477
+
478
+ <div class="form-actions">
479
+ <button type="button" id="quiz-prev-step" class="btn-secondary" style="display:none;">Previous</button>
480
+ <button type="button" id="quiz-next-step" class="btn-primary">Next</button>
481
+ <button type="button" id="quiz-cancel" class="btn-secondary">Cancel</button>
482
+ <button type="submit" id="quiz-submit" class="btn-primary" style="display:none;">Create Quiz</button>
483
+ </div>
484
+ </form>
485
+ </div>
486
+ </div>
487
+
488
+ <!-- Quiz Taking Modal -->
489
+ <div id="quiz-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-title">
490
+ <div class="modal-content quiz-modal-content">
491
+ <div class="modal-header">
492
+ <h2 id="quiz-title">Quiz</h2>
493
+ <div class="quiz-timer" id="quiz-timer">Time: --:--</div>
494
+ </div>
495
+ <div class="quiz-content">
496
+ <div class="quiz-progress">
497
+ <div class="quiz-progress-bar">
498
+ <div class="quiz-progress-fill" id="quiz-progress-fill"></div>
499
+ </div>
500
+ <span class="quiz-progress-text" id="quiz-progress-text">Question 1 of 10</span>
501
+ </div>
502
+
503
+ <div class="quiz-question" id="quiz-question">
504
+ <!-- Question content will be populated here -->
505
+ </div>
506
+
507
+ <div class="quiz-answers" id="quiz-answers">
508
+ <!-- Answer options will be populated here -->
509
+ </div>
510
+
511
+ <div class="quiz-navigation">
512
+ <button id="quiz-prev" class="btn-secondary" disabled>Previous</button>
513
+ <button id="quiz-next" class="btn-primary">Next</button>
514
+ <button id="quiz-submit" class="btn-primary" style="display:none;">Submit Quiz</button>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <!-- Quiz Results Modal -->
521
+ <div id="quiz-results-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="quiz-results-title">
522
+ <div class="modal-content">
523
+ <div class="modal-header">
524
+ <h2 id="quiz-results-title">Quiz Results</h2>
525
+ <p class="modal-subtitle">Your quiz has been completed</p>
526
+ </div>
527
+ <div class="quiz-results-content" id="quiz-results-content">
528
+ <!-- Results will be populated here -->
529
+ </div>
530
+ <div class="form-actions">
531
+ <button type="button" id="quiz-results-close" class="btn-primary">Close</button>
532
+ </div>
533
+ </div>
534
+ </div>
535
+
536
  <!-- Loading Overlay -->
537
  <div id="loading-overlay" class="loading-overlay hidden">
538
  <div class="loading-content">
 
551
  <script src="/static/script.js"></script>
552
  <script src="/static/sessions.js"></script>
553
  <script src="/static/analytics.js"></script>
554
+ <script src="/static/quiz.js"></script>
555
  </body>
556
  </html>
static/quiz.js ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ────────────────────────────── static/quiz.js ──────────────────────────────
2
+ (function() {
3
+ // Quiz state
4
+ let currentQuiz = null;
5
+ let currentQuestionIndex = 0;
6
+ let quizAnswers = {};
7
+ let quizTimer = null;
8
+ let timeRemaining = 0;
9
+ let quizSetupStep = 1;
10
+
11
+ // DOM elements
12
+ const quizLink = document.getElementById('quiz-link');
13
+ const quizSetupModal = document.getElementById('quiz-setup-modal');
14
+ const quizModal = document.getElementById('quiz-modal');
15
+ const quizResultsModal = document.getElementById('quiz-results-modal');
16
+ const quizSetupForm = document.getElementById('quiz-setup-form');
17
+ const quizQuestionsInput = document.getElementById('quiz-questions-input');
18
+ const quizTimeLimit = document.getElementById('quiz-time-limit');
19
+ const quizDocumentList = document.getElementById('quiz-document-list');
20
+ const quizPrevStep = document.getElementById('quiz-prev-step');
21
+ const quizNextStep = document.getElementById('quiz-next-step');
22
+ const quizCancel = document.getElementById('quiz-cancel');
23
+ const quizSubmit = document.getElementById('quiz-submit');
24
+ const quizTimerElement = document.getElementById('quiz-timer');
25
+ const quizProgressFill = document.getElementById('quiz-progress-fill');
26
+ const quizProgressText = document.getElementById('quiz-progress-text');
27
+ const quizQuestion = document.getElementById('quiz-question');
28
+ const quizAnswers = document.getElementById('quiz-answers');
29
+ const quizPrev = document.getElementById('quiz-prev');
30
+ const quizNext = document.getElementById('quiz-next');
31
+ const quizSubmitBtn = document.getElementById('quiz-submit');
32
+ const quizResultsContent = document.getElementById('quiz-results-content');
33
+ const quizResultsClose = document.getElementById('quiz-results-close');
34
+
35
+ // Initialize
36
+ init();
37
+
38
+ function init() {
39
+ setupEventListeners();
40
+ }
41
+
42
+ function setupEventListeners() {
43
+ // Quiz link
44
+ if (quizLink) {
45
+ quizLink.addEventListener('click', (e) => {
46
+ e.preventDefault();
47
+ openQuizSetup();
48
+ });
49
+ }
50
+
51
+ // Quiz setup form
52
+ if (quizSetupForm) {
53
+ quizSetupForm.addEventListener('submit', handleQuizSetupSubmit);
54
+ }
55
+
56
+ // Quiz setup navigation
57
+ if (quizNextStep) {
58
+ quizNextStep.addEventListener('click', nextQuizStep);
59
+ }
60
+ if (quizPrevStep) {
61
+ quizPrevStep.addEventListener('click', prevQuizStep);
62
+ }
63
+ if (quizCancel) {
64
+ quizCancel.addEventListener('click', closeQuizSetup);
65
+ }
66
+
67
+ // Quiz navigation
68
+ if (quizPrev) {
69
+ quizPrev.addEventListener('click', prevQuestion);
70
+ }
71
+ if (quizNext) {
72
+ quizNext.addEventListener('click', nextQuestion);
73
+ }
74
+ if (quizSubmitBtn) {
75
+ quizSubmitBtn.addEventListener('click', submitQuiz);
76
+ }
77
+
78
+ // Quiz results
79
+ if (quizResultsClose) {
80
+ quizResultsClose.addEventListener('click', closeQuizResults);
81
+ }
82
+
83
+ // Close modals on outside click
84
+ document.addEventListener('click', (e) => {
85
+ if (e.target.classList.contains('modal')) {
86
+ closeAllQuizModals();
87
+ }
88
+ });
89
+ }
90
+
91
+ async function openQuizSetup() {
92
+ const user = window.__sb_get_user();
93
+ if (!user) {
94
+ alert('Please sign in to create a quiz');
95
+ window.__sb_show_auth_modal();
96
+ return;
97
+ }
98
+
99
+ const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
100
+ if (!currentProject) {
101
+ alert('Please select a project first');
102
+ return;
103
+ }
104
+
105
+ // Load available documents
106
+ await loadQuizDocuments();
107
+
108
+ // Reset form
109
+ quizSetupStep = 1;
110
+ updateQuizSetupStep();
111
+
112
+ // Show modal
113
+ quizSetupModal.classList.remove('hidden');
114
+ }
115
+
116
+ async function loadQuizDocuments() {
117
+ const user = window.__sb_get_user();
118
+ const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
119
+
120
+ if (!user || !currentProject) return;
121
+
122
+ try {
123
+ const res = await fetch(`/files?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`);
124
+ if (!res.ok) return;
125
+
126
+ const data = await res.json();
127
+ const files = data.files || [];
128
+
129
+ // Clear existing documents
130
+ quizDocumentList.innerHTML = '';
131
+
132
+ if (files.length === 0) {
133
+ quizDocumentList.innerHTML = '<div class="muted">No documents available. Please upload documents first.</div>';
134
+ return;
135
+ }
136
+
137
+ // Create document checkboxes
138
+ files.forEach((file, index) => {
139
+ const item = document.createElement('div');
140
+ item.className = 'document-checkbox-item';
141
+ item.innerHTML = `
142
+ <input type="checkbox" id="quiz-doc-${index}" value="${file.filename}" checked>
143
+ <label for="quiz-doc-${index}">${file.filename}</label>
144
+ `;
145
+ quizDocumentList.appendChild(item);
146
+ });
147
+ } catch (error) {
148
+ console.error('Failed to load documents:', error);
149
+ quizDocumentList.innerHTML = '<div class="muted">Failed to load documents.</div>';
150
+ }
151
+ }
152
+
153
+ function updateQuizSetupStep() {
154
+ // Hide all steps
155
+ document.querySelectorAll('.quiz-step').forEach(step => {
156
+ step.style.display = 'none';
157
+ });
158
+
159
+ // Show current step
160
+ const currentStep = document.getElementById(`quiz-step-${quizSetupStep}`);
161
+ if (currentStep) {
162
+ currentStep.style.display = 'block';
163
+ }
164
+
165
+ // Update navigation buttons
166
+ quizPrevStep.style.display = quizSetupStep > 1 ? 'inline-flex' : 'none';
167
+ quizNextStep.style.display = quizSetupStep < 3 ? 'inline-flex' : 'none';
168
+ quizSubmit.style.display = quizSetupStep === 3 ? 'inline-flex' : 'none';
169
+ }
170
+
171
+ function nextQuizStep() {
172
+ if (quizSetupStep < 3) {
173
+ quizSetupStep++;
174
+ updateQuizSetupStep();
175
+ }
176
+ }
177
+
178
+ function prevQuizStep() {
179
+ if (quizSetupStep > 1) {
180
+ quizSetupStep--;
181
+ updateQuizSetupStep();
182
+ }
183
+ }
184
+
185
+ function closeQuizSetup() {
186
+ quizSetupModal.classList.add('hidden');
187
+ quizSetupStep = 1;
188
+ updateQuizSetupStep();
189
+ }
190
+
191
+ async function handleQuizSetupSubmit(e) {
192
+ e.preventDefault();
193
+
194
+ const user = window.__sb_get_user();
195
+ const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
196
+
197
+ if (!user || !currentProject) {
198
+ alert('Please sign in and select a project');
199
+ return;
200
+ }
201
+
202
+ // Get form data
203
+ const questionsInput = quizQuestionsInput.value.trim();
204
+ const timeLimit = parseInt(quizTimeLimit.value) || 0;
205
+
206
+ // Get selected documents
207
+ const selectedDocs = Array.from(quizDocumentList.querySelectorAll('input[type="checkbox"]:checked'))
208
+ .map(input => input.value);
209
+
210
+ if (selectedDocs.length === 0) {
211
+ alert('Please select at least one document');
212
+ return;
213
+ }
214
+
215
+ if (!questionsInput) {
216
+ alert('Please specify how many questions you want');
217
+ return;
218
+ }
219
+
220
+ // Show loading
221
+ showLoading('Creating quiz...');
222
+
223
+ try {
224
+ // Create quiz
225
+ const formData = new FormData();
226
+ formData.append('user_id', user.user_id);
227
+ formData.append('project_id', currentProject.project_id);
228
+ formData.append('questions_input', questionsInput);
229
+ formData.append('time_limit', timeLimit.toString());
230
+ formData.append('documents', JSON.stringify(selectedDocs));
231
+
232
+ const response = await fetch('/quiz/create', {
233
+ method: 'POST',
234
+ body: formData
235
+ });
236
+
237
+ const data = await response.json();
238
+
239
+ if (response.ok) {
240
+ hideLoading();
241
+ closeQuizSetup();
242
+
243
+ // Start quiz
244
+ currentQuiz = data.quiz;
245
+ currentQuestionIndex = 0;
246
+ quizAnswers = {};
247
+ timeRemaining = timeLimit * 60; // Convert to seconds
248
+
249
+ startQuiz();
250
+ } else {
251
+ hideLoading();
252
+ alert(data.detail || 'Failed to create quiz');
253
+ }
254
+ } catch (error) {
255
+ hideLoading();
256
+ console.error('Quiz creation failed:', error);
257
+ alert('Failed to create quiz. Please try again.');
258
+ }
259
+ }
260
+
261
+ function startQuiz() {
262
+ // Show quiz modal
263
+ quizModal.classList.remove('hidden');
264
+
265
+ // Start timer if time limit is set
266
+ if (timeRemaining > 0) {
267
+ startQuizTimer();
268
+ } else {
269
+ quizTimerElement.textContent = 'No time limit';
270
+ }
271
+
272
+ // Show first question
273
+ showQuestion(0);
274
+ }
275
+
276
+ function startQuizTimer() {
277
+ updateTimerDisplay();
278
+
279
+ quizTimer = setInterval(() => {
280
+ timeRemaining--;
281
+ updateTimerDisplay();
282
+
283
+ if (timeRemaining <= 0) {
284
+ clearInterval(quizTimer);
285
+ timeUp();
286
+ }
287
+ }, 1000);
288
+ }
289
+
290
+ function updateTimerDisplay() {
291
+ const minutes = Math.floor(timeRemaining / 60);
292
+ const seconds = timeRemaining % 60;
293
+ const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
294
+
295
+ quizTimerElement.textContent = `Time: ${timeString}`;
296
+
297
+ // Add warning classes
298
+ quizTimerElement.classList.remove('warning', 'danger');
299
+ if (timeRemaining <= 60) {
300
+ quizTimerElement.classList.add('danger');
301
+ } else if (timeRemaining <= 300) { // 5 minutes
302
+ quizTimerElement.classList.add('warning');
303
+ }
304
+ }
305
+
306
+ function timeUp() {
307
+ alert('Time Up!');
308
+ submitQuiz();
309
+ }
310
+
311
+ function showQuestion(index) {
312
+ if (!currentQuiz || !currentQuiz.questions || index >= currentQuiz.questions.length) {
313
+ return;
314
+ }
315
+
316
+ const question = currentQuiz.questions[index];
317
+ currentQuestionIndex = index;
318
+
319
+ // Update progress
320
+ const progress = ((index + 1) / currentQuiz.questions.length) * 100;
321
+ quizProgressFill.style.width = `${progress}%`;
322
+ quizProgressText.textContent = `Question ${index + 1} of ${currentQuiz.questions.length}`;
323
+
324
+ // Show question
325
+ quizQuestion.innerHTML = `
326
+ <h3>Question ${index + 1}</h3>
327
+ <p>${question.question}</p>
328
+ `;
329
+
330
+ // Show answers
331
+ if (question.type === 'mcq') {
332
+ showMCQAnswers(question);
333
+ } else if (question.type === 'self_reflect') {
334
+ showSelfReflectAnswer(question);
335
+ }
336
+
337
+ // Update navigation
338
+ quizPrev.disabled = index === 0;
339
+ quizNext.style.display = index < currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
340
+ quizSubmitBtn.style.display = index === currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
341
+ }
342
+
343
+ function showMCQAnswers(question) {
344
+ quizAnswers.innerHTML = '';
345
+
346
+ question.options.forEach((option, index) => {
347
+ const optionDiv = document.createElement('div');
348
+ optionDiv.className = 'quiz-answer-option';
349
+ optionDiv.innerHTML = `
350
+ <input type="radio" name="question-${currentQuestionIndex}" value="${index}" id="option-${currentQuestionIndex}-${index}">
351
+ <label for="option-${currentQuestionIndex}-${index}">${option}</label>
352
+ `;
353
+
354
+ // Check if already answered
355
+ if (quizAnswers[currentQuestionIndex] !== undefined) {
356
+ const radio = optionDiv.querySelector('input[type="radio"]');
357
+ radio.checked = quizAnswers[currentQuestionIndex] === index;
358
+ if (radio.checked) {
359
+ optionDiv.classList.add('selected');
360
+ }
361
+ }
362
+
363
+ // Add click handler
364
+ optionDiv.addEventListener('click', () => {
365
+ // Remove selection from other options
366
+ quizAnswers.querySelectorAll('.quiz-answer-option').forEach(opt => {
367
+ opt.classList.remove('selected');
368
+ });
369
+
370
+ // Select this option
371
+ optionDiv.classList.add('selected');
372
+ const radio = optionDiv.querySelector('input[type="radio"]');
373
+ radio.checked = true;
374
+
375
+ // Save answer
376
+ quizAnswers[currentQuestionIndex] = index;
377
+ });
378
+
379
+ quizAnswers.appendChild(optionDiv);
380
+ });
381
+ }
382
+
383
+ function showSelfReflectAnswer(question) {
384
+ quizAnswers.innerHTML = `
385
+ <textarea class="quiz-text-answer" id="self-reflect-${currentQuestionIndex}" placeholder="Enter your answer here...">${quizAnswers[currentQuestionIndex] || ''}</textarea>
386
+ `;
387
+
388
+ const textarea = quizAnswers.querySelector('textarea');
389
+ textarea.addEventListener('input', (e) => {
390
+ quizAnswers[currentQuestionIndex] = e.target.value;
391
+ });
392
+ }
393
+
394
+ function prevQuestion() {
395
+ if (currentQuestionIndex > 0) {
396
+ showQuestion(currentQuestionIndex - 1);
397
+ }
398
+ }
399
+
400
+ function nextQuestion() {
401
+ if (currentQuestionIndex < currentQuiz.questions.length - 1) {
402
+ showQuestion(currentQuestionIndex + 1);
403
+ }
404
+ }
405
+
406
+ async function submitQuiz() {
407
+ if (quizTimer) {
408
+ clearInterval(quizTimer);
409
+ }
410
+
411
+ // Show loading
412
+ showLoading('Submitting quiz...');
413
+
414
+ try {
415
+ const user = window.__sb_get_user();
416
+ const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
417
+
418
+ const formData = new FormData();
419
+ formData.append('user_id', user.user_id);
420
+ formData.append('project_id', currentProject.project_id);
421
+ formData.append('quiz_id', currentQuiz.quiz_id);
422
+ formData.append('answers', JSON.stringify(quizAnswers));
423
+
424
+ const response = await fetch('/quiz/submit', {
425
+ method: 'POST',
426
+ body: formData
427
+ });
428
+
429
+ const data = await response.json();
430
+
431
+ if (response.ok) {
432
+ hideLoading();
433
+ closeQuizModal();
434
+ showQuizResults(data.results);
435
+ } else {
436
+ hideLoading();
437
+ alert(data.detail || 'Failed to submit quiz');
438
+ }
439
+ } catch (error) {
440
+ hideLoading();
441
+ console.error('Quiz submission failed:', error);
442
+ alert('Failed to submit quiz. Please try again.');
443
+ }
444
+ }
445
+
446
+ function showQuizResults(results) {
447
+ // Show results summary
448
+ const totalQuestions = results.questions.length;
449
+ const correctAnswers = results.questions.filter(q => q.status === 'correct').length;
450
+ const partialAnswers = results.questions.filter(q => q.status === 'partial').length;
451
+ const incorrectAnswers = results.questions.filter(q => q.status === 'incorrect').length;
452
+ const score = Math.round((correctAnswers + partialAnswers * 0.5) / totalQuestions * 100);
453
+
454
+ quizResultsContent.innerHTML = `
455
+ <div class="quiz-result-summary">
456
+ <div class="quiz-result-stat">
457
+ <div class="quiz-result-stat-value">${score}%</div>
458
+ <div class="quiz-result-stat-label">Score</div>
459
+ </div>
460
+ <div class="quiz-result-stat">
461
+ <div class="quiz-result-stat-value">${correctAnswers}</div>
462
+ <div class="quiz-result-stat-label">Correct</div>
463
+ </div>
464
+ <div class="quiz-result-stat">
465
+ <div class="quiz-result-stat-value">${partialAnswers}</div>
466
+ <div class="quiz-result-stat-label">Partial</div>
467
+ </div>
468
+ <div class="quiz-result-stat">
469
+ <div class="quiz-result-stat-value">${incorrectAnswers}</div>
470
+ <div class="quiz-result-stat-label">Incorrect</div>
471
+ </div>
472
+ </div>
473
+
474
+ <div class="quiz-result-questions">
475
+ ${results.questions.map((question, index) => `
476
+ <div class="quiz-result-question">
477
+ <div class="quiz-result-question-header">
478
+ <div class="quiz-result-question-title">Question ${index + 1}</div>
479
+ <div class="quiz-result-question-status ${question.status}">${question.status}</div>
480
+ </div>
481
+ <div class="quiz-result-question-text">${question.question}</div>
482
+
483
+ ${question.type === 'mcq' ? `
484
+ <div class="quiz-result-answer">
485
+ <div class="quiz-result-answer-label">Your Answer:</div>
486
+ <div class="quiz-result-answer-content">${question.options[question.user_answer] || 'No answer'}</div>
487
+ </div>
488
+ <div class="quiz-result-answer">
489
+ <div class="quiz-result-answer-label">Correct Answer:</div>
490
+ <div class="quiz-result-answer-content">${question.options[question.correct_answer]}</div>
491
+ </div>
492
+ ` : `
493
+ <div class="quiz-result-answer">
494
+ <div class="quiz-result-answer-label">Your Answer:</div>
495
+ <div class="quiz-result-answer-content">${question.user_answer || 'No answer'}</div>
496
+ </div>
497
+ `}
498
+
499
+ ${question.explanation ? `
500
+ <div class="quiz-result-explanation">
501
+ <strong>Explanation:</strong> ${question.explanation}
502
+ </div>
503
+ ` : ''}
504
+ </div>
505
+ `).join('')}
506
+ </div>
507
+ `;
508
+
509
+ quizResultsModal.classList.remove('hidden');
510
+ }
511
+
512
+ function closeQuizModal() {
513
+ quizModal.classList.add('hidden');
514
+ }
515
+
516
+ function closeQuizResults() {
517
+ quizResultsModal.classList.add('hidden');
518
+ }
519
+
520
+ function closeAllQuizModals() {
521
+ closeQuizSetup();
522
+ closeQuizModal();
523
+ closeQuizResults();
524
+ }
525
+
526
+ function showLoading(message = 'Loading...') {
527
+ const loadingOverlay = document.getElementById('loading-overlay');
528
+ const loadingMessage = document.getElementById('loading-message');
529
+
530
+ if (loadingOverlay && loadingMessage) {
531
+ loadingMessage.textContent = message;
532
+ loadingOverlay.classList.remove('hidden');
533
+ }
534
+ }
535
+
536
+ function hideLoading() {
537
+ const loadingOverlay = document.getElementById('loading-overlay');
538
+ if (loadingOverlay) {
539
+ loadingOverlay.classList.add('hidden');
540
+ }
541
+ }
542
+
543
+ // Expose functions globally
544
+ window.__sb_open_quiz_setup = openQuizSetup;
545
+ window.__sb_close_quiz_setup = closeQuizSetup;
546
+ })();
static/script.js CHANGED
@@ -132,6 +132,18 @@
132
  searchLink.classList.toggle('active');
133
  });
134
  }
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
  function handleFileSelection(files) {
 
132
  searchLink.classList.toggle('active');
133
  });
134
  }
135
+
136
+ // Quiz link toggle
137
+ const quizLink = document.getElementById('quiz-link');
138
+ if (quizLink) {
139
+ quizLink.addEventListener('click', (e) => {
140
+ e.preventDefault();
141
+ // Open quiz setup modal
142
+ if (window.__sb_open_quiz_setup) {
143
+ window.__sb_open_quiz_setup();
144
+ }
145
+ });
146
+ }
147
  }
148
 
149
  function handleFileSelection(files) {
static/styles.css CHANGED
@@ -2424,4 +2424,338 @@
2424
  .daily-label {
2425
  font-size: 0.625rem;
2426
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2427
  }
 
2424
  .daily-label {
2425
  font-size: 0.625rem;
2426
  }
2427
+ }
2428
+
2429
+ /* Quiz Styles */
2430
+ .quiz-modal-content {
2431
+ max-width: 800px;
2432
+ width: 90vw;
2433
+ max-height: 90vh;
2434
+ overflow-y: auto;
2435
+ }
2436
+
2437
+ .quiz-step {
2438
+ margin-bottom: 24px;
2439
+ }
2440
+
2441
+ .document-checkbox-list {
2442
+ display: flex;
2443
+ flex-direction: column;
2444
+ gap: 12px;
2445
+ max-height: 300px;
2446
+ overflow-y: auto;
2447
+ border: 1px solid var(--border);
2448
+ border-radius: var(--radius);
2449
+ padding: 16px;
2450
+ background: var(--bg-secondary);
2451
+ }
2452
+
2453
+ .document-checkbox-item {
2454
+ display: flex;
2455
+ align-items: center;
2456
+ gap: 12px;
2457
+ padding: 8px 12px;
2458
+ background: var(--card);
2459
+ border-radius: var(--radius);
2460
+ border: 1px solid var(--border);
2461
+ transition: all 0.2s ease;
2462
+ }
2463
+
2464
+ .document-checkbox-item:hover {
2465
+ background: var(--card-hover);
2466
+ border-color: var(--border-light);
2467
+ }
2468
+
2469
+ .document-checkbox-item input[type="checkbox"] {
2470
+ margin: 0;
2471
+ transform: scale(1.2);
2472
+ }
2473
+
2474
+ .document-checkbox-item label {
2475
+ flex: 1;
2476
+ margin: 0;
2477
+ cursor: pointer;
2478
+ font-weight: 500;
2479
+ }
2480
+
2481
+ .quiz-timer {
2482
+ font-size: 18px;
2483
+ font-weight: 600;
2484
+ color: var(--accent);
2485
+ background: var(--bg-secondary);
2486
+ padding: 8px 16px;
2487
+ border-radius: var(--radius);
2488
+ border: 1px solid var(--border);
2489
+ }
2490
+
2491
+ .quiz-timer.warning {
2492
+ color: var(--warning);
2493
+ background: rgba(245, 158, 11, 0.1);
2494
+ border-color: var(--warning);
2495
+ }
2496
+
2497
+ .quiz-timer.danger {
2498
+ color: var(--error);
2499
+ background: rgba(239, 68, 68, 0.1);
2500
+ border-color: var(--error);
2501
+ }
2502
+
2503
+ .quiz-progress {
2504
+ margin-bottom: 24px;
2505
+ }
2506
+
2507
+ .quiz-progress-bar {
2508
+ height: 8px;
2509
+ background: var(--border);
2510
+ border-radius: 4px;
2511
+ overflow: hidden;
2512
+ margin-bottom: 8px;
2513
+ }
2514
+
2515
+ .quiz-progress-fill {
2516
+ height: 100%;
2517
+ background: var(--gradient-accent);
2518
+ transition: width 0.3s ease;
2519
+ width: 0%;
2520
+ }
2521
+
2522
+ .quiz-progress-text {
2523
+ font-size: 14px;
2524
+ color: var(--text-secondary);
2525
+ font-weight: 500;
2526
+ }
2527
+
2528
+ .quiz-question {
2529
+ margin-bottom: 24px;
2530
+ padding: 20px;
2531
+ background: var(--card);
2532
+ border-radius: var(--radius-lg);
2533
+ border: 1px solid var(--border);
2534
+ }
2535
+
2536
+ .quiz-question h3 {
2537
+ font-size: 18px;
2538
+ font-weight: 600;
2539
+ margin-bottom: 16px;
2540
+ color: var(--text);
2541
+ }
2542
+
2543
+ .quiz-question p {
2544
+ font-size: 16px;
2545
+ line-height: 1.6;
2546
+ color: var(--text-secondary);
2547
+ margin-bottom: 0;
2548
+ }
2549
+
2550
+ .quiz-answers {
2551
+ margin-bottom: 24px;
2552
+ }
2553
+
2554
+ .quiz-answer-option {
2555
+ display: flex;
2556
+ align-items: flex-start;
2557
+ gap: 12px;
2558
+ padding: 16px;
2559
+ margin-bottom: 12px;
2560
+ background: var(--card);
2561
+ border: 2px solid var(--border);
2562
+ border-radius: var(--radius);
2563
+ cursor: pointer;
2564
+ transition: all 0.2s ease;
2565
+ }
2566
+
2567
+ .quiz-answer-option:hover {
2568
+ background: var(--card-hover);
2569
+ border-color: var(--border-light);
2570
+ }
2571
+
2572
+ .quiz-answer-option.selected {
2573
+ border-color: var(--accent);
2574
+ background: rgba(59, 130, 246, 0.1);
2575
+ }
2576
+
2577
+ .quiz-answer-option.correct {
2578
+ border-color: var(--success);
2579
+ background: rgba(16, 185, 129, 0.1);
2580
+ }
2581
+
2582
+ .quiz-answer-option.incorrect {
2583
+ border-color: var(--error);
2584
+ background: rgba(239, 68, 68, 0.1);
2585
+ }
2586
+
2587
+ .quiz-answer-option input[type="radio"] {
2588
+ margin: 0;
2589
+ transform: scale(1.2);
2590
+ }
2591
+
2592
+ .quiz-answer-option label {
2593
+ flex: 1;
2594
+ margin: 0;
2595
+ cursor: pointer;
2596
+ font-weight: 500;
2597
+ line-height: 1.5;
2598
+ }
2599
+
2600
+ .quiz-text-answer {
2601
+ width: 100%;
2602
+ min-height: 120px;
2603
+ padding: 16px;
2604
+ border: 2px solid var(--border);
2605
+ border-radius: var(--radius);
2606
+ background: var(--card);
2607
+ color: var(--text);
2608
+ font-size: 16px;
2609
+ font-family: inherit;
2610
+ resize: vertical;
2611
+ transition: border-color 0.2s ease;
2612
+ }
2613
+
2614
+ .quiz-text-answer:focus {
2615
+ outline: none;
2616
+ border-color: var(--accent);
2617
+ }
2618
+
2619
+ .quiz-navigation {
2620
+ display: flex;
2621
+ justify-content: space-between;
2622
+ align-items: center;
2623
+ gap: 16px;
2624
+ }
2625
+
2626
+ .quiz-results-content {
2627
+ max-height: 60vh;
2628
+ overflow-y: auto;
2629
+ }
2630
+
2631
+ .quiz-result-summary {
2632
+ display: grid;
2633
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2634
+ gap: 16px;
2635
+ margin-bottom: 24px;
2636
+ }
2637
+
2638
+ .quiz-result-stat {
2639
+ text-align: center;
2640
+ padding: 16px;
2641
+ background: var(--card);
2642
+ border-radius: var(--radius);
2643
+ border: 1px solid var(--border);
2644
+ }
2645
+
2646
+ .quiz-result-stat-value {
2647
+ font-size: 24px;
2648
+ font-weight: 700;
2649
+ color: var(--accent);
2650
+ margin-bottom: 4px;
2651
+ }
2652
+
2653
+ .quiz-result-stat-label {
2654
+ font-size: 14px;
2655
+ color: var(--text-secondary);
2656
+ text-transform: uppercase;
2657
+ letter-spacing: 0.5px;
2658
+ }
2659
+
2660
+ .quiz-result-questions {
2661
+ margin-top: 24px;
2662
+ }
2663
+
2664
+ .quiz-result-question {
2665
+ margin-bottom: 24px;
2666
+ padding: 20px;
2667
+ background: var(--card);
2668
+ border-radius: var(--radius-lg);
2669
+ border: 1px solid var(--border);
2670
+ }
2671
+
2672
+ .quiz-result-question-header {
2673
+ display: flex;
2674
+ justify-content: space-between;
2675
+ align-items: center;
2676
+ margin-bottom: 12px;
2677
+ }
2678
+
2679
+ .quiz-result-question-title {
2680
+ font-weight: 600;
2681
+ color: var(--text);
2682
+ }
2683
+
2684
+ .quiz-result-question-status {
2685
+ padding: 4px 12px;
2686
+ border-radius: 999px;
2687
+ font-size: 12px;
2688
+ font-weight: 600;
2689
+ text-transform: uppercase;
2690
+ }
2691
+
2692
+ .quiz-result-question-status.correct {
2693
+ background: rgba(16, 185, 129, 0.1);
2694
+ color: var(--success);
2695
+ }
2696
+
2697
+ .quiz-result-question-status.incorrect {
2698
+ background: rgba(239, 68, 68, 0.1);
2699
+ color: var(--error);
2700
+ }
2701
+
2702
+ .quiz-result-question-status.partial {
2703
+ background: rgba(245, 158, 11, 0.1);
2704
+ color: var(--warning);
2705
+ }
2706
+
2707
+ .quiz-result-question-text {
2708
+ margin-bottom: 16px;
2709
+ color: var(--text-secondary);
2710
+ line-height: 1.6;
2711
+ }
2712
+
2713
+ .quiz-result-answer {
2714
+ margin-bottom: 12px;
2715
+ padding: 12px;
2716
+ border-radius: var(--radius);
2717
+ border: 1px solid var(--border);
2718
+ }
2719
+
2720
+ .quiz-result-answer-label {
2721
+ font-weight: 600;
2722
+ margin-bottom: 4px;
2723
+ color: var(--text);
2724
+ }
2725
+
2726
+ .quiz-result-answer-content {
2727
+ color: var(--text-secondary);
2728
+ line-height: 1.5;
2729
+ }
2730
+
2731
+ .quiz-result-explanation {
2732
+ margin-top: 12px;
2733
+ padding: 12px;
2734
+ background: var(--bg-secondary);
2735
+ border-radius: var(--radius);
2736
+ border-left: 4px solid var(--accent);
2737
+ font-size: 14px;
2738
+ color: var(--text-secondary);
2739
+ line-height: 1.5;
2740
+ }
2741
+
2742
+ /* Quiz responsive */
2743
+ @media (max-width: 768px) {
2744
+ .quiz-modal-content {
2745
+ width: 95vw;
2746
+ max-height: 95vh;
2747
+ }
2748
+
2749
+ .quiz-navigation {
2750
+ flex-direction: column;
2751
+ gap: 12px;
2752
+ }
2753
+
2754
+ .quiz-navigation button {
2755
+ width: 100%;
2756
+ }
2757
+
2758
+ .quiz-result-summary {
2759
+ grid-template-columns: 1fr;
2760
+ }
2761
  }
utils/rag/rag.py CHANGED
@@ -78,6 +78,29 @@ class RAGStore:
78
  return serializable_doc
79
  return None
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def list_files(self, user_id: str, project_id: str):
82
  """List all files for a project with their summaries"""
83
  files_cursor = self.files.find(
 
78
  return serializable_doc
79
  return None
80
 
81
+ def get_file_chunks(self, user_id: str, project_id: str, filename: str, limit: int = 20) -> List[Dict[str, Any]]:
82
+ """Get chunks for a specific file"""
83
+ cursor = self.chunks.find({
84
+ "user_id": user_id,
85
+ "project_id": project_id,
86
+ "filename": filename
87
+ }).limit(limit)
88
+
89
+ chunks = []
90
+ for doc in cursor:
91
+ # Convert MongoDB document to JSON-serializable format
92
+ serializable_doc = {}
93
+ for key, value in doc.items():
94
+ if key == '_id':
95
+ serializable_doc[key] = str(value)
96
+ elif hasattr(value, 'isoformat'):
97
+ serializable_doc[key] = value.isoformat()
98
+ else:
99
+ serializable_doc[key] = value
100
+ chunks.append(serializable_doc)
101
+
102
+ return chunks
103
+
104
  def list_files(self, user_id: str, project_id: str):
105
  """List all files for a project with their summaries"""
106
  files_cursor = self.files.find(