LiamKhoaLe commited on
Commit
4d8a8da
·
1 Parent(s): 4b83d69

Enh quiz mode #2

Browse files
Files changed (3) hide show
  1. routes/quiz.py +69 -11
  2. static/quiz.js +117 -19
  3. static/styles.css +169 -3
routes/quiz.py CHANGED
@@ -175,15 +175,21 @@ async def get_document_summaries(user_id: str, project_id: str, documents: List[
175
  summaries.append(f"[{doc}] {file_data['summary']}")
176
 
177
  # Get additional chunks for more context
178
- chunks = rag.get_file_chunks(user_id=user_id, project_id=project_id, filename=doc, limit=30)
179
  if chunks:
180
- chunk_text = "\n".join([chunk.get("content", "") for chunk in chunks[:15]])
181
- summaries.append(f"[{doc} - Additional Content] {chunk_text[:3000]}...")
 
 
 
182
 
183
  except Exception as e:
184
  logger.warning(f"[QUIZ] Failed to get summary for {doc}: {e}")
185
  continue
186
 
 
 
 
187
  # Use NVIDIA_LARGE for comprehensive analysis if content is long
188
  combined_summaries = "\n\n".join(summaries)
189
  if len(combined_summaries) > 5000:
@@ -364,21 +370,31 @@ async def generate_questions_and_answers(plan: List[Dict], document_summaries: s
364
 
365
  async def generate_task_questions(task: Dict, document_summaries: str, nvidia_rotator, complexity: str = None) -> List[Dict]:
366
  """Generate questions for a specific task using appropriate model based on complexity"""
367
- system_prompt = f"""You are an expert quiz question generator.
368
  Generate {task.get('number_mcq', 0)} multiple choice questions and {task.get('number_sr', 0)} self-reflection questions.
369
 
370
  For MCQ questions:
371
- - Create clear, well-structured questions
372
- - Provide 4 answer options (A, B, C, D)
373
- - Mark the correct answer
374
- - Make distractors plausible but incorrect
375
  - Focus on the specific topic: {task.get('topic', 'General')}
 
 
376
 
377
  For self-reflection questions:
378
  - Create open-ended questions that require critical thinking
379
  - Focus on analysis, evaluation, and synthesis
380
  - Encourage personal reflection and application
381
  - Relate to the specific topic: {task.get('topic', 'General')}
 
 
 
 
 
 
 
 
382
 
383
  Return a JSON array of question objects with this format:
384
  {{
@@ -392,9 +408,9 @@ async def generate_task_questions(task: Dict, document_summaries: str, nvidia_ro
392
  """
393
 
394
  user_prompt = f"""Task: {task}
395
- Document content: {document_summaries[:4000]}...
396
 
397
- Generate questions:"""
398
 
399
  try:
400
  # Use appropriate model based on complexity
@@ -416,13 +432,55 @@ async def generate_task_questions(task: Dict, document_summaries: str, nvidia_ro
416
  )
417
 
418
  questions = json.loads(response.strip())
419
- return questions if isinstance(questions, list) else []
 
 
 
 
 
 
 
 
 
420
 
421
  except Exception as e:
422
  logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
423
  return []
424
 
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  async def mark_quiz_answers(questions: List[Dict], user_answers: Dict, nvidia_rotator) -> Dict:
427
  """Mark quiz answers and provide feedback"""
428
  results = {
 
175
  summaries.append(f"[{doc}] {file_data['summary']}")
176
 
177
  # Get additional chunks for more context
178
+ chunks = rag.get_file_chunks(user_id=user_id, project_id=project_id, filename=doc, limit=50)
179
  if chunks:
180
+ # Filter out very short chunks and prioritize longer, more informative ones
181
+ meaningful_chunks = [chunk for chunk in chunks if len(chunk.get("content", "")) > 100]
182
+ if meaningful_chunks:
183
+ chunk_text = "\n".join([chunk.get("content", "") for chunk in meaningful_chunks[:20]])
184
+ summaries.append(f"[{doc} - Detailed Content] {chunk_text[:4000]}...")
185
 
186
  except Exception as e:
187
  logger.warning(f"[QUIZ] Failed to get summary for {doc}: {e}")
188
  continue
189
 
190
+ if not summaries:
191
+ raise HTTPException(400, detail="No content found in selected documents. Please ensure documents contain text content.")
192
+
193
  # Use NVIDIA_LARGE for comprehensive analysis if content is long
194
  combined_summaries = "\n\n".join(summaries)
195
  if len(combined_summaries) > 5000:
 
370
 
371
  async def generate_task_questions(task: Dict, document_summaries: str, nvidia_rotator, complexity: str = None) -> List[Dict]:
372
  """Generate questions for a specific task using appropriate model based on complexity"""
373
+ system_prompt = f"""You are an expert quiz question generator creating high-quality educational questions.
374
  Generate {task.get('number_mcq', 0)} multiple choice questions and {task.get('number_sr', 0)} self-reflection questions.
375
 
376
  For MCQ questions:
377
+ - Create clear, well-structured questions that test understanding
378
+ - Provide 4 answer options (A, B, C, D) that are plausible
379
+ - Mark the correct answer (0-3 index)
380
+ - Make distractors plausible but clearly incorrect
381
  - Focus on the specific topic: {task.get('topic', 'General')}
382
+ - Ensure questions are answerable from the provided content
383
+ - Avoid trivial or overly obvious questions
384
 
385
  For self-reflection questions:
386
  - Create open-ended questions that require critical thinking
387
  - Focus on analysis, evaluation, and synthesis
388
  - Encourage personal reflection and application
389
  - Relate to the specific topic: {task.get('topic', 'General')}
390
+ - Make questions thought-provoking but not overly complex
391
+ - Ensure they can be answered based on the provided content
392
+
393
+ Quality Requirements:
394
+ - Questions must be directly answerable from the provided content
395
+ - Avoid questions that require external knowledge
396
+ - Ensure clarity and proper grammar
397
+ - Make questions educational and meaningful
398
 
399
  Return a JSON array of question objects with this format:
400
  {{
 
408
  """
409
 
410
  user_prompt = f"""Task: {task}
411
+ Document content: {document_summaries[:5000]}...
412
 
413
+ Generate high-quality questions based ONLY on the provided content:"""
414
 
415
  try:
416
  # Use appropriate model based on complexity
 
432
  )
433
 
434
  questions = json.loads(response.strip())
435
+
436
+ # Validate questions
437
+ validated_questions = []
438
+ for question in questions:
439
+ if validate_question(question):
440
+ validated_questions.append(question)
441
+ else:
442
+ logger.warning(f"[QUIZ] Invalid question filtered out: {question}")
443
+
444
+ return validated_questions
445
 
446
  except Exception as e:
447
  logger.warning(f"[QUIZ] Failed to generate questions for task: {e}")
448
  return []
449
 
450
 
451
+ def validate_question(question: Dict) -> bool:
452
+ """Validate that a question meets quality standards"""
453
+ try:
454
+ # Check required fields
455
+ if not question.get("type") or question["type"] not in ["mcq", "self_reflect"]:
456
+ return False
457
+
458
+ if not question.get("question") or len(question["question"]) < 10:
459
+ return False
460
+
461
+ if not question.get("topic"):
462
+ return False
463
+
464
+ # Validate MCQ questions
465
+ if question["type"] == "mcq":
466
+ options = question.get("options", [])
467
+ if len(options) != 4:
468
+ return False
469
+
470
+ correct_answer = question.get("correct_answer")
471
+ if correct_answer is None or not isinstance(correct_answer, int) or correct_answer < 0 or correct_answer > 3:
472
+ return False
473
+
474
+ # Check for empty options
475
+ if any(not option or len(option.strip()) < 1 for option in options):
476
+ return False
477
+
478
+ return True
479
+
480
+ except Exception:
481
+ return False
482
+
483
+
484
  async def mark_quiz_answers(questions: List[Dict], user_answers: Dict, nvidia_rotator) -> Dict:
485
  """Mark quiz answers and provide feedback"""
486
  results = {
static/quiz.js CHANGED
@@ -195,7 +195,7 @@
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
 
@@ -208,17 +208,24 @@
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
@@ -229,6 +236,8 @@
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
@@ -237,6 +246,11 @@
237
  const data = await response.json();
238
 
239
  if (response.ok) {
 
 
 
 
 
240
  hideLoading();
241
  closeQuizSetup();
242
 
@@ -246,15 +260,16 @@
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
 
@@ -408,13 +423,31 @@
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);
@@ -429,17 +462,22 @@
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
 
@@ -451,22 +489,37 @@
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>
@@ -483,12 +536,14 @@
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>
@@ -507,6 +562,9 @@
507
  `;
508
 
509
  quizResultsModal.classList.remove('hidden');
 
 
 
510
  }
511
 
512
  function closeQuizModal() {
@@ -533,6 +591,19 @@
533
  }
534
  }
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  function hideLoading() {
537
  const loadingOverlay = document.getElementById('loading-overlay');
538
  if (loadingOverlay) {
@@ -540,6 +611,33 @@
540
  }
541
  }
542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  // Expose functions globally
544
  window.__sb_open_quiz_setup = openQuizSetup;
545
  window.__sb_close_quiz_setup = closeQuizSetup;
 
195
  const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
196
 
197
  if (!user || !currentProject) {
198
+ showNotification('Please sign in and select a project', 'error');
199
  return;
200
  }
201
 
 
208
  .map(input => input.value);
209
 
210
  if (selectedDocs.length === 0) {
211
+ showNotification('Please select at least one document', 'warning');
212
  return;
213
  }
214
 
215
  if (!questionsInput) {
216
+ showNotification('Please specify how many questions you want', 'warning');
217
  return;
218
  }
219
 
220
+ // Validate questions input
221
+ if (questionsInput.length < 10) {
222
+ showNotification('Please provide more details about your question requirements', 'warning');
223
+ return;
224
+ }
225
+
226
+ // Show loading with progress updates
227
  showLoading('Creating quiz...');
228
+ updateLoadingProgress('Parsing your requirements...', 20);
229
 
230
  try {
231
  // Create quiz
 
236
  formData.append('time_limit', timeLimit.toString());
237
  formData.append('documents', JSON.stringify(selectedDocs));
238
 
239
+ updateLoadingProgress('Analyzing documents...', 40);
240
+
241
  const response = await fetch('/quiz/create', {
242
  method: 'POST',
243
  body: formData
 
246
  const data = await response.json();
247
 
248
  if (response.ok) {
249
+ updateLoadingProgress('Generating questions...', 80);
250
+
251
+ // Simulate processing time for better UX
252
+ await new Promise(resolve => setTimeout(resolve, 1000));
253
+
254
  hideLoading();
255
  closeQuizSetup();
256
 
 
260
  quizAnswers = {};
261
  timeRemaining = timeLimit * 60; // Convert to seconds
262
 
263
+ showNotification(`Quiz created successfully! ${currentQuiz.questions.length} questions generated.`, 'success');
264
  startQuiz();
265
  } else {
266
  hideLoading();
267
+ showNotification(data.detail || 'Failed to create quiz', 'error');
268
  }
269
  } catch (error) {
270
  hideLoading();
271
  console.error('Quiz creation failed:', error);
272
+ showNotification('Failed to create quiz. Please try again.', 'error');
273
  }
274
  }
275
 
 
423
  clearInterval(quizTimer);
424
  }
425
 
426
+ // Validate that all questions are answered
427
+ const unansweredQuestions = [];
428
+ for (let i = 0; i < currentQuiz.questions.length; i++) {
429
+ if (quizAnswers[i] === undefined || quizAnswers[i] === null || quizAnswers[i] === '') {
430
+ unansweredQuestions.push(i + 1);
431
+ }
432
+ }
433
+
434
+ if (unansweredQuestions.length > 0) {
435
+ const confirmSubmit = confirm(`You have ${unansweredQuestions.length} unanswered questions (${unansweredQuestions.join(', ')}). Do you want to submit anyway?`);
436
+ if (!confirmSubmit) {
437
+ return;
438
+ }
439
+ }
440
+
441
+ // Show loading with progress
442
  showLoading('Submitting quiz...');
443
+ updateLoadingProgress('Processing your answers...', 30);
444
 
445
  try {
446
  const user = window.__sb_get_user();
447
  const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
448
 
449
+ updateLoadingProgress('Marking your answers...', 60);
450
+
451
  const formData = new FormData();
452
  formData.append('user_id', user.user_id);
453
  formData.append('project_id', currentProject.project_id);
 
462
  const data = await response.json();
463
 
464
  if (response.ok) {
465
+ updateLoadingProgress('Generating feedback...', 90);
466
+
467
+ // Simulate processing time for better UX
468
+ await new Promise(resolve => setTimeout(resolve, 1500));
469
+
470
  hideLoading();
471
  closeQuizModal();
472
  showQuizResults(data.results);
473
  } else {
474
  hideLoading();
475
+ showNotification(data.detail || 'Failed to submit quiz', 'error');
476
  }
477
  } catch (error) {
478
  hideLoading();
479
  console.error('Quiz submission failed:', error);
480
+ showNotification('Failed to submit quiz. Please try again.', 'error');
481
  }
482
  }
483
 
 
489
  const incorrectAnswers = results.questions.filter(q => q.status === 'incorrect').length;
490
  const score = Math.round((correctAnswers + partialAnswers * 0.5) / totalQuestions * 100);
491
 
492
+ // Determine performance level
493
+ let performanceLevel = 'Needs Improvement';
494
+ let performanceColor = 'var(--error)';
495
+ if (score >= 90) {
496
+ performanceLevel = 'Excellent';
497
+ performanceColor = 'var(--success)';
498
+ } else if (score >= 80) {
499
+ performanceLevel = 'Good';
500
+ performanceColor = 'var(--accent)';
501
+ } else if (score >= 70) {
502
+ performanceLevel = 'Satisfactory';
503
+ performanceColor = 'var(--warning)';
504
+ }
505
+
506
  quizResultsContent.innerHTML = `
507
  <div class="quiz-result-summary">
508
+ <div class="quiz-result-stat quiz-result-score">
509
+ <div class="quiz-result-stat-value" style="color: ${performanceColor}">${score}%</div>
510
  <div class="quiz-result-stat-label">Score</div>
511
+ <div class="quiz-result-performance">${performanceLevel}</div>
512
  </div>
513
  <div class="quiz-result-stat">
514
+ <div class="quiz-result-stat-value" style="color: var(--success)">${correctAnswers}</div>
515
  <div class="quiz-result-stat-label">Correct</div>
516
  </div>
517
  <div class="quiz-result-stat">
518
+ <div class="quiz-result-stat-value" style="color: var(--warning)">${partialAnswers}</div>
519
  <div class="quiz-result-stat-label">Partial</div>
520
  </div>
521
  <div class="quiz-result-stat">
522
+ <div class="quiz-result-stat-value" style="color: var(--error)">${incorrectAnswers}</div>
523
  <div class="quiz-result-stat-label">Incorrect</div>
524
  </div>
525
  </div>
 
536
  ${question.type === 'mcq' ? `
537
  <div class="quiz-result-answer">
538
  <div class="quiz-result-answer-label">Your Answer:</div>
539
+ <div class="quiz-result-answer-content ${question.status === 'correct' ? 'correct' : question.status === 'incorrect' ? 'incorrect' : ''}">${question.options[question.user_answer] || 'No answer'}</div>
 
 
 
 
540
  </div>
541
+ ${question.status === 'incorrect' ? `
542
+ <div class="quiz-result-answer">
543
+ <div class="quiz-result-answer-label">Correct Answer:</div>
544
+ <div class="quiz-result-answer-content correct">${question.options[question.correct_answer]}</div>
545
+ </div>
546
+ ` : ''}
547
  ` : `
548
  <div class="quiz-result-answer">
549
  <div class="quiz-result-answer-label">Your Answer:</div>
 
562
  `;
563
 
564
  quizResultsModal.classList.remove('hidden');
565
+
566
+ // Show success notification
567
+ showNotification(`Quiz completed! You scored ${score}% (${performanceLevel})`, 'success');
568
  }
569
 
570
  function closeQuizModal() {
 
591
  }
592
  }
593
 
594
+ function updateLoadingProgress(message, progress) {
595
+ const loadingOverlay = document.getElementById('loading-overlay');
596
+ const loadingMessage = document.getElementById('loading-message');
597
+ const loadingProgress = document.getElementById('loading-progress');
598
+
599
+ if (loadingOverlay && loadingMessage) {
600
+ loadingMessage.textContent = message;
601
+ if (loadingProgress) {
602
+ loadingProgress.style.width = `${progress}%`;
603
+ }
604
+ }
605
+ }
606
+
607
  function hideLoading() {
608
  const loadingOverlay = document.getElementById('loading-overlay');
609
  if (loadingOverlay) {
 
611
  }
612
  }
613
 
614
+ function showNotification(message, type = 'info') {
615
+ // Create notification element
616
+ const notification = document.createElement('div');
617
+ notification.className = `notification notification-${type}`;
618
+ notification.innerHTML = `
619
+ <div class="notification-content">
620
+ <span class="notification-message">${message}</span>
621
+ <button class="notification-close">&times;</button>
622
+ </div>
623
+ `;
624
+
625
+ // Add to page
626
+ document.body.appendChild(notification);
627
+
628
+ // Auto remove after 5 seconds
629
+ setTimeout(() => {
630
+ if (notification.parentNode) {
631
+ notification.remove();
632
+ }
633
+ }, 5000);
634
+
635
+ // Close button
636
+ notification.querySelector('.notification-close').addEventListener('click', () => {
637
+ notification.remove();
638
+ });
639
+ }
640
+
641
  // Expose functions globally
642
  window.__sb_open_quiz_setup = openQuizSetup;
643
  window.__sb_close_quiz_setup = closeQuizSetup;
static/styles.css CHANGED
@@ -2428,14 +2428,36 @@
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 {
@@ -2515,8 +2537,26 @@
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 {
@@ -2561,7 +2601,24 @@
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 {
@@ -2657,6 +2714,20 @@
2657
  letter-spacing: 0.5px;
2658
  }
2659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2660
  .quiz-result-questions {
2661
  margin-top: 24px;
2662
  }
@@ -2728,6 +2799,24 @@
2728
  line-height: 1.5;
2729
  }
2730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2731
  .quiz-result-explanation {
2732
  margin-top: 12px;
2733
  padding: 12px;
@@ -2758,4 +2847,81 @@
2758
  .quiz-result-summary {
2759
  grid-template-columns: 1fr;
2760
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2761
  }
 
2428
 
2429
  /* Quiz Styles */
2430
  .quiz-modal-content {
2431
+ max-width: 900px;
2432
  width: 90vw;
2433
  max-height: 90vh;
2434
  overflow-y: auto;
2435
+ animation: slideInUp 0.3s ease-out;
2436
+ }
2437
+
2438
+ @keyframes slideInUp {
2439
+ from {
2440
+ opacity: 0;
2441
+ transform: translateY(30px);
2442
+ }
2443
+ to {
2444
+ opacity: 1;
2445
+ transform: translateY(0);
2446
+ }
2447
  }
2448
 
2449
  .quiz-step {
2450
  margin-bottom: 24px;
2451
+ opacity: 0;
2452
+ transform: translateX(20px);
2453
+ animation: fadeInSlide 0.4s ease-out forwards;
2454
+ }
2455
+
2456
+ @keyframes fadeInSlide {
2457
+ to {
2458
+ opacity: 1;
2459
+ transform: translateX(0);
2460
+ }
2461
  }
2462
 
2463
  .document-checkbox-list {
 
2537
  .quiz-progress-fill {
2538
  height: 100%;
2539
  background: var(--gradient-accent);
2540
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
2541
  width: 0%;
2542
+ position: relative;
2543
+ overflow: hidden;
2544
+ }
2545
+
2546
+ .quiz-progress-fill::after {
2547
+ content: '';
2548
+ position: absolute;
2549
+ top: 0;
2550
+ left: -100%;
2551
+ width: 100%;
2552
+ height: 100%;
2553
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
2554
+ animation: progressShimmer 2s infinite;
2555
+ }
2556
+
2557
+ @keyframes progressShimmer {
2558
+ 0% { left: -100%; }
2559
+ 100% { left: 100%; }
2560
  }
2561
 
2562
  .quiz-progress-text {
 
2601
  border: 2px solid var(--border);
2602
  border-radius: var(--radius);
2603
  cursor: pointer;
2604
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2605
+ position: relative;
2606
+ overflow: hidden;
2607
+ }
2608
+
2609
+ .quiz-answer-option::before {
2610
+ content: '';
2611
+ position: absolute;
2612
+ top: 0;
2613
+ left: -100%;
2614
+ width: 100%;
2615
+ height: 100%;
2616
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
2617
+ transition: left 0.5s ease;
2618
+ }
2619
+
2620
+ .quiz-answer-option:hover::before {
2621
+ left: 100%;
2622
  }
2623
 
2624
  .quiz-answer-option:hover {
 
2714
  letter-spacing: 0.5px;
2715
  }
2716
 
2717
+ .quiz-result-performance {
2718
+ font-size: 12px;
2719
+ font-weight: 600;
2720
+ margin-top: 4px;
2721
+ text-transform: uppercase;
2722
+ letter-spacing: 0.5px;
2723
+ }
2724
+
2725
+ .quiz-result-score {
2726
+ border: 2px solid var(--border);
2727
+ border-radius: var(--radius);
2728
+ background: var(--card);
2729
+ }
2730
+
2731
  .quiz-result-questions {
2732
  margin-top: 24px;
2733
  }
 
2799
  line-height: 1.5;
2800
  }
2801
 
2802
+ .quiz-result-answer-content.correct {
2803
+ color: var(--success);
2804
+ font-weight: 600;
2805
+ background: rgba(16, 185, 129, 0.1);
2806
+ padding: 8px;
2807
+ border-radius: 4px;
2808
+ border-left: 3px solid var(--success);
2809
+ }
2810
+
2811
+ .quiz-result-answer-content.incorrect {
2812
+ color: var(--error);
2813
+ font-weight: 600;
2814
+ background: rgba(239, 68, 68, 0.1);
2815
+ padding: 8px;
2816
+ border-radius: 4px;
2817
+ border-left: 3px solid var(--error);
2818
+ }
2819
+
2820
  .quiz-result-explanation {
2821
  margin-top: 12px;
2822
  padding: 12px;
 
2847
  .quiz-result-summary {
2848
  grid-template-columns: 1fr;
2849
  }
2850
+ }
2851
+
2852
+ /* Notification Styles */
2853
+ .notification {
2854
+ position: fixed;
2855
+ top: 20px;
2856
+ right: 20px;
2857
+ z-index: 10000;
2858
+ max-width: 400px;
2859
+ padding: 16px;
2860
+ border-radius: var(--radius);
2861
+ box-shadow: var(--shadow-lg);
2862
+ animation: slideInRight 0.3s ease-out;
2863
+ border-left: 4px solid;
2864
+ }
2865
+
2866
+ .notification-success {
2867
+ background: var(--card);
2868
+ border-left-color: var(--success);
2869
+ color: var(--text);
2870
+ }
2871
+
2872
+ .notification-error {
2873
+ background: var(--card);
2874
+ border-left-color: var(--error);
2875
+ color: var(--text);
2876
+ }
2877
+
2878
+ .notification-warning {
2879
+ background: var(--card);
2880
+ border-left-color: var(--warning);
2881
+ color: var(--text);
2882
+ }
2883
+
2884
+ .notification-info {
2885
+ background: var(--card);
2886
+ border-left-color: var(--accent);
2887
+ color: var(--text);
2888
+ }
2889
+
2890
+ .notification-content {
2891
+ display: flex;
2892
+ align-items: center;
2893
+ justify-content: space-between;
2894
+ gap: 12px;
2895
+ }
2896
+
2897
+ .notification-message {
2898
+ flex: 1;
2899
+ font-weight: 500;
2900
+ }
2901
+
2902
+ .notification-close {
2903
+ background: none;
2904
+ border: none;
2905
+ font-size: 18px;
2906
+ cursor: pointer;
2907
+ color: var(--text-secondary);
2908
+ padding: 4px;
2909
+ border-radius: 4px;
2910
+ transition: all 0.2s ease;
2911
+ }
2912
+
2913
+ .notification-close:hover {
2914
+ background: var(--bg-secondary);
2915
+ color: var(--text);
2916
+ }
2917
+
2918
+ @keyframes slideInRight {
2919
+ from {
2920
+ opacity: 0;
2921
+ transform: translateX(100%);
2922
+ }
2923
+ to {
2924
+ opacity: 1;
2925
+ transform: translateX(0);
2926
+ }
2927
  }