Spaces:
Sleeping
Sleeping
Commit
·
4d8a8da
1
Parent(s):
4b83d69
Enh quiz mode #2
Browse files- routes/quiz.py +69 -11
- static/quiz.js +117 -19
- 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=
|
| 179 |
if chunks:
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
| 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[:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 199 |
return;
|
| 200 |
}
|
| 201 |
|
|
@@ -208,17 +208,24 @@
|
|
| 208 |
.map(input => input.value);
|
| 209 |
|
| 210 |
if (selectedDocs.length === 0) {
|
| 211 |
-
|
| 212 |
return;
|
| 213 |
}
|
| 214 |
|
| 215 |
if (!questionsInput) {
|
| 216 |
-
|
| 217 |
return;
|
| 218 |
}
|
| 219 |
|
| 220 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 253 |
}
|
| 254 |
} catch (error) {
|
| 255 |
hideLoading();
|
| 256 |
console.error('Quiz creation failed:', error);
|
| 257 |
-
|
| 258 |
}
|
| 259 |
}
|
| 260 |
|
|
@@ -408,13 +423,31 @@
|
|
| 408 |
clearInterval(quizTimer);
|
| 409 |
}
|
| 410 |
|
| 411 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 438 |
}
|
| 439 |
} catch (error) {
|
| 440 |
hideLoading();
|
| 441 |
console.error('Quiz submission failed:', error);
|
| 442 |
-
|
| 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">×</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:
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|