Spaces:
Sleeping
Sleeping
Fix timer and add multi-answer question support
Browse files- Replace broken JS DOM-based timer with gr.Timer (server-side 1s tick);
handles countdown display and auto-submit on expiry
- Add gr.CheckboxGroup alongside gr.Radio; questions with list
correct_answer show the checkbox variant with "Select all that apply"
- Update correct_answer to arrays for question ids 19, 37, 44, 46, 54
- Grade by comparing sorted answer lists so order doesn't matter
- Add CLAUDE.md with architecture and dev notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md +48 -0
- app.py +154 -185
- questionnaire.json +5 -5
CLAUDE.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md
|
| 2 |
+
|
| 3 |
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
| 4 |
+
|
| 5 |
+
## Running the App
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -r requirements.txt
|
| 9 |
+
python app.py
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
The app is also deployed to Hugging Face Spaces (configured via the YAML frontmatter in README.md), where it runs automatically on push.
|
| 13 |
+
|
| 14 |
+
## Architecture
|
| 15 |
+
|
| 16 |
+
This is a single-file Gradio app (`app.py`) with a JSON question bank (`questionnaire.json`).
|
| 17 |
+
|
| 18 |
+
**Data flow:**
|
| 19 |
+
- `questionnaire.json` — source of truth. Has two top-level keys: `exam_info` (metadata, passing score, certifications) and `questions` (array of 67 question objects). Each question has `options` (list of strings), `correct_answer` (0-indexed int), and `explanation`.
|
| 20 |
+
- All questions are loaded at module startup into `QUESTIONS` and `EXAM_INFO` globals.
|
| 21 |
+
|
| 22 |
+
**State management:**
|
| 23 |
+
- All quiz state lives in Gradio `gr.State` components: `quiz_questions`, `quiz_start_time`, `quiz_active`, `current_question_idx`, `user_answers`.
|
| 24 |
+
- `user_answers` is a list of 10 elements, each `None` or a 0-indexed integer matching the selected option.
|
| 25 |
+
- Answers are stored by parsing the first character of the Radio choice string (e.g., `"A. Some option"` → `ord('A') - 65 = 0`).
|
| 26 |
+
|
| 27 |
+
**Timer:**
|
| 28 |
+
- The countdown timer is client-side JavaScript injected via `demo.load(..., js=...)`. It uses a `MutationObserver` to detect when Start/Restart buttons appear, then sets an interval to update the DOM directly. When time runs out, it programmatically clicks the Submit button.
|
| 29 |
+
- The Python server has no timer polling; `quiz_start_time` state exists but the actual countdown is purely frontend.
|
| 30 |
+
|
| 31 |
+
**Navigation pattern:**
|
| 32 |
+
- `navigate_question(direction, ...)` handles Prev/Next (hardcoded to 10 questions, index 0–9).
|
| 33 |
+
- `jump_to_question(question_label, ...)` parses `"Question N"` strings from the Dropdown.
|
| 34 |
+
- Both restore the previously saved answer when loading a question.
|
| 35 |
+
|
| 36 |
+
## Adding Questions
|
| 37 |
+
|
| 38 |
+
Add objects to the `questions` array in `questionnaire.json`. Each question must follow this schema:
|
| 39 |
+
```json
|
| 40 |
+
{
|
| 41 |
+
"id": <int>,
|
| 42 |
+
"section": "<category string>",
|
| 43 |
+
"question": "<question text>",
|
| 44 |
+
"options": ["<A>", "<B>", "<C>", "<D>"],
|
| 45 |
+
"correct_answer": <0-indexed int>,
|
| 46 |
+
"explanation": "<shown only for wrong answers>"
|
| 47 |
+
}
|
| 48 |
+
```
|
app.py
CHANGED
|
@@ -2,7 +2,6 @@ import gradio as gr
|
|
| 2 |
import json
|
| 3 |
import random
|
| 4 |
import time
|
| 5 |
-
from typing import Optional
|
| 6 |
|
| 7 |
# Load questions from JSON file
|
| 8 |
def load_questions():
|
|
@@ -13,42 +12,49 @@ def load_questions():
|
|
| 13 |
QUESTIONS, EXAM_INFO = load_questions()
|
| 14 |
TIME_LIMIT = 5 * 60 # 5 minutes in seconds
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
def select_random_questions(num_questions: int = 10) -> list:
|
| 17 |
-
"""Select random questions from the pool."""
|
| 18 |
return random.sample(QUESTIONS, min(num_questions, len(QUESTIONS)))
|
| 19 |
-
|
| 20 |
-
def format_question(question: dict, index: int) -> str:
|
| 21 |
-
""
|
| 22 |
-
return f"**Question {index + 1}** ({question['section']})\n\n{question['question']}"
|
| 23 |
|
| 24 |
def calculate_time_remaining(start_time: float) -> tuple[int, int, bool]:
|
| 25 |
-
"""Calculate remaining time and check if expired."""
|
| 26 |
elapsed = time.time() - start_time
|
| 27 |
remaining = max(0, TIME_LIMIT - elapsed)
|
| 28 |
minutes = int(remaining // 60)
|
| 29 |
seconds = int(remaining % 60)
|
| 30 |
-
|
| 31 |
-
return minutes, seconds, expired
|
| 32 |
|
| 33 |
def grade_quiz(selected_questions: list, user_answers: list) -> tuple[int, int, list]:
|
| 34 |
-
"""Grade the quiz and return score and details."""
|
| 35 |
correct_count = 0
|
| 36 |
results = []
|
| 37 |
|
| 38 |
for i, (question, answer) in enumerate(zip(selected_questions, user_answers)):
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
if is_correct:
|
| 42 |
correct_count += 1
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
results.append({
|
| 45 |
"question_num": i + 1,
|
| 46 |
"question": question["question"],
|
| 47 |
"section": question["section"],
|
| 48 |
-
"
|
| 49 |
-
"
|
| 50 |
-
"correct_answer": correct_idx,
|
| 51 |
-
"correct_answer_text": question["options"][correct_idx],
|
| 52 |
"is_correct": is_correct,
|
| 53 |
"explanation": question["explanation"]
|
| 54 |
})
|
|
@@ -56,11 +62,10 @@ def grade_quiz(selected_questions: list, user_answers: list) -> tuple[int, int,
|
|
| 56 |
return correct_count, len(selected_questions), results
|
| 57 |
|
| 58 |
def format_results(correct: int, total: int, results: list) -> str:
|
| 59 |
-
"""Format the quiz results for display."""
|
| 60 |
percentage = (correct / total) * 100
|
| 61 |
passing = percentage >= EXAM_INFO["passing_score"]
|
| 62 |
|
| 63 |
-
output =
|
| 64 |
output += f"## Score: {correct}/{total} ({percentage:.1f}%)\n\n"
|
| 65 |
output += f"**Status: {'PASSED' if passing else 'FAILED'}** (Passing score: {EXAM_INFO['passing_score']}%)\n\n"
|
| 66 |
output += "---\n\n"
|
|
@@ -70,20 +75,35 @@ def format_results(correct: int, total: int, results: list) -> str:
|
|
| 70 |
output += f"### Question {result['question_num']} {status_icon}\n"
|
| 71 |
output += f"**Section:** {result['section']}\n\n"
|
| 72 |
output += f"**Question:** {result['question']}\n\n"
|
| 73 |
-
|
| 74 |
-
if result["user_answer"] is not None:
|
| 75 |
-
output += f"**Your answer:** {chr(65 + result['user_answer'])}. {result['user_answer_text']}\n\n"
|
| 76 |
-
else:
|
| 77 |
-
output += f"**Your answer:** No answer provided\n\n"
|
| 78 |
|
| 79 |
if not result["is_correct"]:
|
| 80 |
-
output += f"**Correct answer:** {
|
| 81 |
output += f"**Explanation:** {result['explanation']}\n\n"
|
| 82 |
|
| 83 |
output += "---\n\n"
|
| 84 |
|
| 85 |
return output
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
# Create Gradio interface
|
| 88 |
with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
|
| 89 |
# State variables
|
|
@@ -91,7 +111,7 @@ with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
|
|
| 91 |
quiz_start_time = gr.State(0.0)
|
| 92 |
quiz_active = gr.State(False)
|
| 93 |
current_question_idx = gr.State(0)
|
| 94 |
-
user_answers = gr.State([
|
| 95 |
|
| 96 |
gr.Markdown(f"""
|
| 97 |
# {EXAM_INFO['title']}
|
|
@@ -121,10 +141,9 @@ with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
|
|
| 121 |
|
| 122 |
question_display = gr.Markdown("", elem_id="question-display")
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
label="Select
|
| 127 |
-
interactive=True
|
| 128 |
)
|
| 129 |
|
| 130 |
with gr.Row():
|
|
@@ -135,215 +154,165 @@ with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
|
|
| 135 |
results_section = gr.Markdown(visible=False)
|
| 136 |
restart_btn = gr.Button("Start New Quiz", visible=False, variant="primary")
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
def start_quiz():
|
| 139 |
-
"""Initialize a new quiz."""
|
| 140 |
questions = select_random_questions(10)
|
| 141 |
start_time = time.time()
|
| 142 |
-
answers = [
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(first_question["options"])]
|
| 147 |
|
| 148 |
return (
|
| 149 |
-
questions,
|
| 150 |
-
start_time,
|
| 151 |
-
True,
|
| 152 |
-
0,
|
| 153 |
-
answers,
|
| 154 |
-
gr.update(visible=True),
|
| 155 |
-
question_text,
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
gr.update(visible=False),
|
| 159 |
-
gr.update(visible=False),
|
| 160 |
-
|
| 161 |
-
"**
|
| 162 |
-
|
|
|
|
| 163 |
)
|
| 164 |
|
| 165 |
def navigate_question(direction, current_idx, questions, answers):
|
| 166 |
-
|
| 167 |
-
# Calculate new index
|
| 168 |
-
if direction == "prev":
|
| 169 |
-
new_idx = max(0, current_idx - 1)
|
| 170 |
-
else:
|
| 171 |
-
new_idx = min(9, current_idx + 1)
|
| 172 |
-
|
| 173 |
-
# Load new question
|
| 174 |
question = questions[new_idx]
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
# Set previous answer if exists
|
| 179 |
-
prev_answer = None
|
| 180 |
-
if answers[new_idx] is not None:
|
| 181 |
-
prev_answer = choices[answers[new_idx]]
|
| 182 |
-
|
| 183 |
-
answered_count = sum(1 for a in answers if a is not None)
|
| 184 |
-
|
| 185 |
return (
|
| 186 |
new_idx,
|
| 187 |
-
|
| 188 |
-
|
|
|
|
| 189 |
f"**Answered:** {answered_count}/10",
|
| 190 |
-
gr.update(value=f"Question {new_idx + 1}")
|
| 191 |
)
|
| 192 |
|
| 193 |
def jump_to_question(question_label, current_idx, questions, answers):
|
| 194 |
-
"""Jump to a specific question."""
|
| 195 |
-
# Parse question number
|
| 196 |
new_idx = int(question_label.split()[1]) - 1
|
| 197 |
-
|
| 198 |
-
# Load question
|
| 199 |
question = questions[new_idx]
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
# Set previous answer if exists
|
| 204 |
-
prev_answer = None
|
| 205 |
-
if answers[new_idx] is not None:
|
| 206 |
-
prev_answer = choices[answers[new_idx]]
|
| 207 |
-
|
| 208 |
-
answered_count = sum(1 for a in answers if a is not None)
|
| 209 |
-
|
| 210 |
return (
|
| 211 |
new_idx,
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
| 215 |
)
|
| 216 |
|
| 217 |
-
def
|
| 218 |
-
"""Save the current answer."""
|
| 219 |
answers = answers.copy()
|
| 220 |
if answer is not None:
|
| 221 |
-
|
| 222 |
-
|
|
|
|
| 223 |
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
| 225 |
return answers, f"**Answered:** {answered_count}/{len(answers)}"
|
| 226 |
|
| 227 |
-
def
|
| 228 |
-
"""Submit the quiz and show results."""
|
| 229 |
correct, total, results = grade_quiz(questions, answers)
|
| 230 |
results_text = format_results(correct, total, results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
return (
|
| 233 |
-
|
| 234 |
-
gr.update(
|
| 235 |
-
gr.update(visible=True),
|
| 236 |
-
gr.update(visible=
|
| 237 |
-
False,
|
| 238 |
-
|
| 239 |
)
|
| 240 |
|
| 241 |
-
#
|
| 242 |
-
start_btn.click(
|
| 243 |
-
start_quiz,
|
| 244 |
-
outputs=[
|
| 245 |
-
quiz_questions, quiz_start_time, quiz_active, current_question_idx, user_answers,
|
| 246 |
-
quiz_section, question_display, answer_choices, results_section, restart_btn,
|
| 247 |
-
start_btn, timer_display, answer_status, question_selector
|
| 248 |
-
]
|
| 249 |
-
)
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
)
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
prev_btn.click(
|
| 261 |
-
lambda *
|
| 262 |
inputs=[current_question_idx, quiz_questions, user_answers],
|
| 263 |
-
outputs=
|
| 264 |
)
|
| 265 |
-
|
| 266 |
next_btn.click(
|
| 267 |
-
lambda *
|
| 268 |
inputs=[current_question_idx, quiz_questions, user_answers],
|
| 269 |
-
outputs=
|
| 270 |
)
|
| 271 |
|
| 272 |
question_selector.change(
|
| 273 |
jump_to_question,
|
| 274 |
inputs=[question_selector, current_question_idx, quiz_questions, user_answers],
|
| 275 |
-
outputs=[current_question_idx, question_display,
|
| 276 |
)
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
inputs=[
|
| 281 |
-
outputs=[user_answers, answer_status]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
)
|
| 283 |
|
| 284 |
submit_btn.click(
|
| 285 |
-
|
| 286 |
inputs=[quiz_questions, user_answers],
|
| 287 |
-
outputs=[quiz_section, results_section, restart_btn, start_btn, quiz_active, timer_display]
|
| 288 |
)
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
None,
|
| 295 |
-
js="""
|
| 296 |
-
() => {
|
| 297 |
-
let timerInterval = null;
|
| 298 |
-
let startTime = null;
|
| 299 |
-
const timeLimit = 5 * 60 * 1000; // 5 minutes in milliseconds
|
| 300 |
-
|
| 301 |
-
function updateTimer() {
|
| 302 |
-
if (!startTime) return;
|
| 303 |
-
|
| 304 |
-
const elapsed = Date.now() - startTime;
|
| 305 |
-
const remaining = Math.max(0, timeLimit - elapsed);
|
| 306 |
-
const minutes = Math.floor(remaining / 60000);
|
| 307 |
-
const seconds = Math.floor((remaining % 60000) / 1000);
|
| 308 |
-
|
| 309 |
-
const timerEl = document.querySelector('[data-testid="markdown"] p strong');
|
| 310 |
-
if (timerEl && timerEl.textContent.includes('Time')) {
|
| 311 |
-
timerEl.parentElement.innerHTML = `<strong>Time Remaining:</strong> ${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
if (remaining <= 0) {
|
| 315 |
-
clearInterval(timerInterval);
|
| 316 |
-
// Trigger submit button click
|
| 317 |
-
const submitBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Submit'));
|
| 318 |
-
if (submitBtn) submitBtn.click();
|
| 319 |
-
}
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
// Watch for quiz start
|
| 323 |
-
const observer = new MutationObserver(() => {
|
| 324 |
-
const startBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent === 'Start Quiz');
|
| 325 |
-
const restartBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent === 'Start New Quiz');
|
| 326 |
-
|
| 327 |
-
if (startBtn) {
|
| 328 |
-
startBtn.addEventListener('click', () => {
|
| 329 |
-
startTime = Date.now();
|
| 330 |
-
if (timerInterval) clearInterval(timerInterval);
|
| 331 |
-
timerInterval = setInterval(updateTimer, 1000);
|
| 332 |
-
}, { once: true });
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
if (restartBtn) {
|
| 336 |
-
restartBtn.addEventListener('click', () => {
|
| 337 |
-
startTime = Date.now();
|
| 338 |
-
if (timerInterval) clearInterval(timerInterval);
|
| 339 |
-
timerInterval = setInterval(updateTimer, 1000);
|
| 340 |
-
});
|
| 341 |
-
}
|
| 342 |
-
});
|
| 343 |
-
|
| 344 |
-
observer.observe(document.body, { childList: true, subtree: true });
|
| 345 |
-
}
|
| 346 |
-
"""
|
| 347 |
)
|
| 348 |
|
| 349 |
if __name__ == "__main__":
|
|
|
|
| 2 |
import json
|
| 3 |
import random
|
| 4 |
import time
|
|
|
|
| 5 |
|
| 6 |
# Load questions from JSON file
|
| 7 |
def load_questions():
|
|
|
|
| 12 |
QUESTIONS, EXAM_INFO = load_questions()
|
| 13 |
TIME_LIMIT = 5 * 60 # 5 minutes in seconds
|
| 14 |
|
| 15 |
+
def is_multi_answer(question: dict) -> bool:
|
| 16 |
+
return isinstance(question["correct_answer"], list)
|
| 17 |
+
|
| 18 |
def select_random_questions(num_questions: int = 10) -> list:
|
|
|
|
| 19 |
return random.sample(QUESTIONS, min(num_questions, len(QUESTIONS)))
|
| 20 |
+
|
| 21 |
+
def format_question(question: dict, index: int) -> str:
|
| 22 |
+
suffix = " *(Select all that apply)*" if is_multi_answer(question) else ""
|
| 23 |
+
return f"**Question {index + 1}** ({question['section']}){suffix}\n\n{question['question']}"
|
| 24 |
|
| 25 |
def calculate_time_remaining(start_time: float) -> tuple[int, int, bool]:
|
|
|
|
| 26 |
elapsed = time.time() - start_time
|
| 27 |
remaining = max(0, TIME_LIMIT - elapsed)
|
| 28 |
minutes = int(remaining // 60)
|
| 29 |
seconds = int(remaining % 60)
|
| 30 |
+
return minutes, seconds, remaining <= 0
|
|
|
|
| 31 |
|
| 32 |
def grade_quiz(selected_questions: list, user_answers: list) -> tuple[int, int, list]:
|
|
|
|
| 33 |
correct_count = 0
|
| 34 |
results = []
|
| 35 |
|
| 36 |
for i, (question, answer) in enumerate(zip(selected_questions, user_answers)):
|
| 37 |
+
correct = question["correct_answer"]
|
| 38 |
+
if isinstance(correct, int):
|
| 39 |
+
correct = [correct]
|
| 40 |
+
|
| 41 |
+
is_correct = sorted(answer or []) == sorted(correct)
|
| 42 |
if is_correct:
|
| 43 |
correct_count += 1
|
| 44 |
|
| 45 |
+
user_ans_text = ", ".join(
|
| 46 |
+
f"{chr(65+a)}. {question['options'][a]}" for a in (answer or [])
|
| 47 |
+
) or "No answer provided"
|
| 48 |
+
correct_ans_text = ", ".join(
|
| 49 |
+
f"{chr(65+a)}. {question['options'][a]}" for a in correct
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
results.append({
|
| 53 |
"question_num": i + 1,
|
| 54 |
"question": question["question"],
|
| 55 |
"section": question["section"],
|
| 56 |
+
"user_answer_text": user_ans_text,
|
| 57 |
+
"correct_answer_text": correct_ans_text,
|
|
|
|
|
|
|
| 58 |
"is_correct": is_correct,
|
| 59 |
"explanation": question["explanation"]
|
| 60 |
})
|
|
|
|
| 62 |
return correct_count, len(selected_questions), results
|
| 63 |
|
| 64 |
def format_results(correct: int, total: int, results: list) -> str:
|
|
|
|
| 65 |
percentage = (correct / total) * 100
|
| 66 |
passing = percentage >= EXAM_INFO["passing_score"]
|
| 67 |
|
| 68 |
+
output = "# Quiz Results\n\n"
|
| 69 |
output += f"## Score: {correct}/{total} ({percentage:.1f}%)\n\n"
|
| 70 |
output += f"**Status: {'PASSED' if passing else 'FAILED'}** (Passing score: {EXAM_INFO['passing_score']}%)\n\n"
|
| 71 |
output += "---\n\n"
|
|
|
|
| 75 |
output += f"### Question {result['question_num']} {status_icon}\n"
|
| 76 |
output += f"**Section:** {result['section']}\n\n"
|
| 77 |
output += f"**Question:** {result['question']}\n\n"
|
| 78 |
+
output += f"**Your answer:** {result['user_answer_text']}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
if not result["is_correct"]:
|
| 81 |
+
output += f"**Correct answer:** {result['correct_answer_text']}\n\n"
|
| 82 |
output += f"**Explanation:** {result['explanation']}\n\n"
|
| 83 |
|
| 84 |
output += "---\n\n"
|
| 85 |
|
| 86 |
return output
|
| 87 |
|
| 88 |
+
def get_answer_components(question: dict, answers: list, q_idx: int):
|
| 89 |
+
"""Return (radio_update, checkbox_update) for displaying a question."""
|
| 90 |
+
choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(question["options"])]
|
| 91 |
+
saved = answers[q_idx] or []
|
| 92 |
+
|
| 93 |
+
if is_multi_answer(question):
|
| 94 |
+
prev = [choices[i] for i in saved]
|
| 95 |
+
return (
|
| 96 |
+
gr.update(choices=choices, value=None, visible=False),
|
| 97 |
+
gr.update(choices=choices, value=prev, visible=True),
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
prev = choices[saved[0]] if saved else None
|
| 101 |
+
return (
|
| 102 |
+
gr.update(choices=choices, value=prev, visible=True),
|
| 103 |
+
gr.update(choices=[], value=[], visible=False),
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
# Create Gradio interface
|
| 108 |
with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
|
| 109 |
# State variables
|
|
|
|
| 111 |
quiz_start_time = gr.State(0.0)
|
| 112 |
quiz_active = gr.State(False)
|
| 113 |
current_question_idx = gr.State(0)
|
| 114 |
+
user_answers = gr.State([[] for _ in range(10)])
|
| 115 |
|
| 116 |
gr.Markdown(f"""
|
| 117 |
# {EXAM_INFO['title']}
|
|
|
|
| 141 |
|
| 142 |
question_display = gr.Markdown("", elem_id="question-display")
|
| 143 |
|
| 144 |
+
answer_radio = gr.Radio(choices=[], label="Select your answer:", interactive=True)
|
| 145 |
+
answer_checkbox = gr.CheckboxGroup(
|
| 146 |
+
choices=[], label="Select all that apply:", interactive=True, visible=False
|
|
|
|
| 147 |
)
|
| 148 |
|
| 149 |
with gr.Row():
|
|
|
|
| 154 |
results_section = gr.Markdown(visible=False)
|
| 155 |
restart_btn = gr.Button("Start New Quiz", visible=False, variant="primary")
|
| 156 |
|
| 157 |
+
quiz_timer = gr.Timer(value=1)
|
| 158 |
+
|
| 159 |
+
# --- Event handler functions ---
|
| 160 |
+
|
| 161 |
def start_quiz():
|
|
|
|
| 162 |
questions = select_random_questions(10)
|
| 163 |
start_time = time.time()
|
| 164 |
+
answers = [[] for _ in range(10)]
|
| 165 |
|
| 166 |
+
question_text = format_question(questions[0], 0)
|
| 167 |
+
radio_update, checkbox_update = get_answer_components(questions[0], answers, 0)
|
|
|
|
| 168 |
|
| 169 |
return (
|
| 170 |
+
questions, # quiz_questions
|
| 171 |
+
start_time, # quiz_start_time
|
| 172 |
+
True, # quiz_active
|
| 173 |
+
0, # current_question_idx
|
| 174 |
+
answers, # user_answers
|
| 175 |
+
gr.update(visible=True), # quiz_section
|
| 176 |
+
question_text, # question_display
|
| 177 |
+
radio_update, # answer_radio
|
| 178 |
+
checkbox_update, # answer_checkbox
|
| 179 |
+
gr.update(visible=False), # results_section
|
| 180 |
+
gr.update(visible=False), # restart_btn
|
| 181 |
+
gr.update(visible=False), # start_btn
|
| 182 |
+
"**Time Remaining:** 05:00", # timer_display
|
| 183 |
+
"**Answered:** 0/10", # answer_status
|
| 184 |
+
gr.update(value="Question 1"), # question_selector
|
| 185 |
)
|
| 186 |
|
| 187 |
def navigate_question(direction, current_idx, questions, answers):
|
| 188 |
+
new_idx = max(0, current_idx - 1) if direction == "prev" else min(9, current_idx + 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
question = questions[new_idx]
|
| 190 |
+
radio_update, checkbox_update = get_answer_components(question, answers, new_idx)
|
| 191 |
+
answered_count = sum(1 for a in answers if a)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
return (
|
| 193 |
new_idx,
|
| 194 |
+
format_question(question, new_idx),
|
| 195 |
+
radio_update,
|
| 196 |
+
checkbox_update,
|
| 197 |
f"**Answered:** {answered_count}/10",
|
| 198 |
+
gr.update(value=f"Question {new_idx + 1}"),
|
| 199 |
)
|
| 200 |
|
| 201 |
def jump_to_question(question_label, current_idx, questions, answers):
|
|
|
|
|
|
|
| 202 |
new_idx = int(question_label.split()[1]) - 1
|
|
|
|
|
|
|
| 203 |
question = questions[new_idx]
|
| 204 |
+
radio_update, checkbox_update = get_answer_components(question, answers, new_idx)
|
| 205 |
+
answered_count = sum(1 for a in answers if a)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
return (
|
| 207 |
new_idx,
|
| 208 |
+
format_question(question, new_idx),
|
| 209 |
+
radio_update,
|
| 210 |
+
checkbox_update,
|
| 211 |
+
f"**Answered:** {answered_count}/10",
|
| 212 |
)
|
| 213 |
|
| 214 |
+
def save_answer_radio(answer, current_idx, answers):
|
|
|
|
| 215 |
answers = answers.copy()
|
| 216 |
if answer is not None:
|
| 217 |
+
answers[current_idx] = [ord(answer[0]) - 65]
|
| 218 |
+
answered_count = sum(1 for a in answers if a)
|
| 219 |
+
return answers, f"**Answered:** {answered_count}/{len(answers)}"
|
| 220 |
|
| 221 |
+
def save_answer_checkbox(selected, current_idx, answers):
|
| 222 |
+
answers = answers.copy()
|
| 223 |
+
answers[current_idx] = sorted([ord(s[0]) - 65 for s in selected]) if selected else []
|
| 224 |
+
answered_count = sum(1 for a in answers if a)
|
| 225 |
return answers, f"**Answered:** {answered_count}/{len(answers)}"
|
| 226 |
|
| 227 |
+
def do_submit(questions, answers):
|
|
|
|
| 228 |
correct, total, results = grade_quiz(questions, answers)
|
| 229 |
results_text = format_results(correct, total, results)
|
| 230 |
+
return (
|
| 231 |
+
gr.update(visible=False), # quiz_section
|
| 232 |
+
gr.update(value=results_text, visible=True), # results_section
|
| 233 |
+
gr.update(visible=True), # restart_btn
|
| 234 |
+
gr.update(visible=False), # start_btn
|
| 235 |
+
False, # quiz_active
|
| 236 |
+
"**Quiz Submitted**", # timer_display
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
def tick_timer(quiz_active_val, start_time_val, questions, answers):
|
| 240 |
+
if not quiz_active_val:
|
| 241 |
+
return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), quiz_active_val)
|
| 242 |
|
| 243 |
+
minutes, seconds, expired = calculate_time_remaining(start_time_val)
|
| 244 |
+
|
| 245 |
+
if not expired:
|
| 246 |
+
return (
|
| 247 |
+
f"**Time Remaining:** {minutes:02d}:{seconds:02d}",
|
| 248 |
+
gr.update(), gr.update(), gr.update(), gr.update(),
|
| 249 |
+
quiz_active_val,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Auto-submit on expiry
|
| 253 |
+
correct, total, results = grade_quiz(questions, answers)
|
| 254 |
+
results_text = format_results(correct, total, results)
|
| 255 |
return (
|
| 256 |
+
"**Time Expired**",
|
| 257 |
+
gr.update(visible=False),
|
| 258 |
+
gr.update(value=results_text, visible=True),
|
| 259 |
+
gr.update(visible=True),
|
| 260 |
+
gr.update(visible=False),
|
| 261 |
+
False,
|
| 262 |
)
|
| 263 |
|
| 264 |
+
# --- Wire up events ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
+
start_outputs = [
|
| 267 |
+
quiz_questions, quiz_start_time, quiz_active, current_question_idx, user_answers,
|
| 268 |
+
quiz_section, question_display, answer_radio, answer_checkbox,
|
| 269 |
+
results_section, restart_btn, start_btn, timer_display, answer_status, question_selector,
|
| 270 |
+
]
|
| 271 |
+
start_btn.click(start_quiz, outputs=start_outputs)
|
| 272 |
+
restart_btn.click(start_quiz, outputs=start_outputs)
|
|
|
|
| 273 |
|
| 274 |
+
nav_outputs = [
|
| 275 |
+
current_question_idx, question_display, answer_radio, answer_checkbox,
|
| 276 |
+
answer_status, question_selector,
|
| 277 |
+
]
|
| 278 |
prev_btn.click(
|
| 279 |
+
lambda *a: navigate_question("prev", *a),
|
| 280 |
inputs=[current_question_idx, quiz_questions, user_answers],
|
| 281 |
+
outputs=nav_outputs,
|
| 282 |
)
|
|
|
|
| 283 |
next_btn.click(
|
| 284 |
+
lambda *a: navigate_question("next", *a),
|
| 285 |
inputs=[current_question_idx, quiz_questions, user_answers],
|
| 286 |
+
outputs=nav_outputs,
|
| 287 |
)
|
| 288 |
|
| 289 |
question_selector.change(
|
| 290 |
jump_to_question,
|
| 291 |
inputs=[question_selector, current_question_idx, quiz_questions, user_answers],
|
| 292 |
+
outputs=[current_question_idx, question_display, answer_radio, answer_checkbox, answer_status],
|
| 293 |
)
|
| 294 |
|
| 295 |
+
answer_radio.change(
|
| 296 |
+
save_answer_radio,
|
| 297 |
+
inputs=[answer_radio, current_question_idx, user_answers],
|
| 298 |
+
outputs=[user_answers, answer_status],
|
| 299 |
+
)
|
| 300 |
+
answer_checkbox.change(
|
| 301 |
+
save_answer_checkbox,
|
| 302 |
+
inputs=[answer_checkbox, current_question_idx, user_answers],
|
| 303 |
+
outputs=[user_answers, answer_status],
|
| 304 |
)
|
| 305 |
|
| 306 |
submit_btn.click(
|
| 307 |
+
do_submit,
|
| 308 |
inputs=[quiz_questions, user_answers],
|
| 309 |
+
outputs=[quiz_section, results_section, restart_btn, start_btn, quiz_active, timer_display],
|
| 310 |
)
|
| 311 |
|
| 312 |
+
quiz_timer.tick(
|
| 313 |
+
tick_timer,
|
| 314 |
+
inputs=[quiz_active, quiz_start_time, quiz_questions, user_answers],
|
| 315 |
+
outputs=[timer_display, quiz_section, results_section, restart_btn, start_btn, quiz_active],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
)
|
| 317 |
|
| 318 |
if __name__ == "__main__":
|
questionnaire.json
CHANGED
|
@@ -256,7 +256,7 @@
|
|
| 256 |
"GPUs connected via PCIe Gen3 instead of Gen4",
|
| 257 |
"CPU using older DDR4 memory"
|
| 258 |
],
|
| 259 |
-
"correct_answer": 1,
|
| 260 |
"explanation": "The correct answers are B and C. Inefficient data loading creates a pipeline stall where the GPU waits for data. PCIe Gen3 (16 GB/s) vs Gen4 (32 GB/s) can bottleneck transfers to GPU memory, especially for high-bandwidth GPUs like A100."
|
| 261 |
},
|
| 262 |
{
|
|
@@ -490,7 +490,7 @@
|
|
| 490 |
"Power plan set to Power Saver",
|
| 491 |
"Maya scene contains corrupted geometry"
|
| 492 |
],
|
| 493 |
-
"correct_answer": 0,
|
| 494 |
"explanation": "The correct answers are A and C. PCIe Gen3 vs Gen4 significantly impacts bandwidth (16 GB/s vs 32 GB/s). Power Saver mode limits GPU clock speeds and performance. Both directly reduce rendering performance."
|
| 495 |
},
|
| 496 |
{
|
|
@@ -581,7 +581,7 @@
|
|
| 581 |
"PCIe Gen3 instead of Gen4",
|
| 582 |
"CPU using DDR4 memory"
|
| 583 |
],
|
| 584 |
-
"correct_answer": 1,
|
| 585 |
"explanation": "The correct answers are B and C. Inefficient data loading creates pipeline stalls where GPU waits for data. PCIe Gen3 (16 GB/s) vs Gen4 (32 GB/s) limits host-to-GPU transfer bandwidth, bottlenecking memory-intensive operations."
|
| 586 |
},
|
| 587 |
{
|
|
@@ -607,7 +607,7 @@
|
|
| 607 |
"Increase system RAM",
|
| 608 |
"Configure NCCL for correct network interface"
|
| 609 |
],
|
| 610 |
-
"correct_answer": 0,
|
| 611 |
"explanation": "The correct answers are A, B, and D. PCIe peer-to-peer enables direct GPU-to-GPU transfers. InfiniBand/RoCE provide high-bandwidth, low-latency networking. Proper NCCL configuration ensures optimal use of available network interfaces. Increasing system RAM doesn't improve inter-GPU communication."
|
| 612 |
},
|
| 613 |
{
|
|
@@ -711,7 +711,7 @@
|
|
| 711 |
"Implement async prefetching with torch.Generator",
|
| 712 |
"Use faster storage (NVMe vs HDD)"
|
| 713 |
],
|
| 714 |
-
"correct_answer": 0,
|
| 715 |
"explanation": "The correct answers are A and D. DataLoader with multiple workers parallelizes data loading. Faster storage (NVMe) directly improves I/O performance. Loading entire dataset to RAM is impractical for large datasets. torch.Generator is for reproducibility, not async prefetching."
|
| 716 |
},
|
| 717 |
{
|
|
|
|
| 256 |
"GPUs connected via PCIe Gen3 instead of Gen4",
|
| 257 |
"CPU using older DDR4 memory"
|
| 258 |
],
|
| 259 |
+
"correct_answer": [1, 2],
|
| 260 |
"explanation": "The correct answers are B and C. Inefficient data loading creates a pipeline stall where the GPU waits for data. PCIe Gen3 (16 GB/s) vs Gen4 (32 GB/s) can bottleneck transfers to GPU memory, especially for high-bandwidth GPUs like A100."
|
| 261 |
},
|
| 262 |
{
|
|
|
|
| 490 |
"Power plan set to Power Saver",
|
| 491 |
"Maya scene contains corrupted geometry"
|
| 492 |
],
|
| 493 |
+
"correct_answer": [0, 2],
|
| 494 |
"explanation": "The correct answers are A and C. PCIe Gen3 vs Gen4 significantly impacts bandwidth (16 GB/s vs 32 GB/s). Power Saver mode limits GPU clock speeds and performance. Both directly reduce rendering performance."
|
| 495 |
},
|
| 496 |
{
|
|
|
|
| 581 |
"PCIe Gen3 instead of Gen4",
|
| 582 |
"CPU using DDR4 memory"
|
| 583 |
],
|
| 584 |
+
"correct_answer": [1, 2],
|
| 585 |
"explanation": "The correct answers are B and C. Inefficient data loading creates pipeline stalls where GPU waits for data. PCIe Gen3 (16 GB/s) vs Gen4 (32 GB/s) limits host-to-GPU transfer bandwidth, bottlenecking memory-intensive operations."
|
| 586 |
},
|
| 587 |
{
|
|
|
|
| 607 |
"Increase system RAM",
|
| 608 |
"Configure NCCL for correct network interface"
|
| 609 |
],
|
| 610 |
+
"correct_answer": [0, 1, 3],
|
| 611 |
"explanation": "The correct answers are A, B, and D. PCIe peer-to-peer enables direct GPU-to-GPU transfers. InfiniBand/RoCE provide high-bandwidth, low-latency networking. Proper NCCL configuration ensures optimal use of available network interfaces. Increasing system RAM doesn't improve inter-GPU communication."
|
| 612 |
},
|
| 613 |
{
|
|
|
|
| 711 |
"Implement async prefetching with torch.Generator",
|
| 712 |
"Use faster storage (NVMe vs HDD)"
|
| 713 |
],
|
| 714 |
+
"correct_answer": [0, 3],
|
| 715 |
"explanation": "The correct answers are A and D. DataLoader with multiple workers parallelizes data loading. Faster storage (NVMe) directly improves I/O performance. Loading entire dataset to RAM is impractical for large datasets. torch.Generator is for reproducibility, not async prefetching."
|
| 716 |
},
|
| 717 |
{
|