tog Claude Sonnet 4.6 commited on
Commit
37ab661
·
1 Parent(s): 664a34b

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>

Files changed (3) hide show
  1. CLAUDE.md +48 -0
  2. app.py +154 -185
  3. 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
- """Format a question for display."""
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
- expired = remaining <= 0
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
- correct_idx = question["correct_answer"]
40
- is_correct = answer == correct_idx
 
 
 
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
- "user_answer": answer,
49
- "user_answer_text": question["options"][answer] if answer is not None else "No answer",
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 = f"# Quiz Results\n\n"
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:** {chr(65 + result['correct_answer'])}. {result['correct_answer_text']}\n\n"
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([None] * 10)
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
- answer_choices = gr.Radio(
125
- choices=[],
126
- label="Select your answer:",
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 = [None] * 10
143
 
144
- first_question = questions[0]
145
- question_text = format_question(first_question, 0)
146
- choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(first_question["options"])]
147
 
148
  return (
149
- questions, # quiz_questions
150
- start_time, # quiz_start_time
151
- True, # quiz_active
152
- 0, # current_question_idx
153
- answers, # user_answers
154
- gr.update(visible=True), # quiz_section
155
- question_text, # question_display
156
- gr.update(choices=choices, value=None, interactive=True), # answer_choices
157
- gr.update(visible=False), # results_section
158
- gr.update(visible=False), # restart_btn
159
- gr.update(visible=False), # start_btn
160
- "**Time Remaining:** 05:00", # timer_display
161
- "**Answered:** 0/10", # answer_status
162
- gr.update(value="Question 1") # question_selector
 
163
  )
164
 
165
  def navigate_question(direction, current_idx, questions, answers):
166
- """Navigate to previous or next question."""
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
- question_text = format_question(question, new_idx)
176
- choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(question["options"])]
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
- question_text,
188
- gr.update(choices=choices, value=prev_answer),
 
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
- question_text = format_question(question, new_idx)
201
- choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(question["options"])]
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
- question_text,
213
- gr.update(choices=choices, value=prev_answer),
214
- f"**Answered:** {answered_count}/10"
 
215
  )
216
 
217
- def save_answer(answer, current_idx, answers):
218
- """Save the current answer."""
219
  answers = answers.copy()
220
  if answer is not None:
221
- answer_idx = ord(answer[0]) - 65
222
- answers[current_idx] = answer_idx
 
223
 
224
- answered_count = sum(1 for a in answers if a is not None)
 
 
 
225
  return answers, f"**Answered:** {answered_count}/{len(answers)}"
226
 
227
- def submit_quiz(questions, answers):
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
- gr.update(visible=False), # quiz_section
234
- gr.update(value=results_text, visible=True), # results_section
235
- gr.update(visible=True), # restart_btn
236
- gr.update(visible=False), # start_btn
237
- False, # quiz_active
238
- "**Quiz Submitted**" # timer_display
239
  )
240
 
241
- # Event handlers
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
- restart_btn.click(
252
- start_quiz,
253
- outputs=[
254
- quiz_questions, quiz_start_time, quiz_active, current_question_idx, user_answers,
255
- quiz_section, question_display, answer_choices, results_section, restart_btn,
256
- start_btn, timer_display, answer_status, question_selector
257
- ]
258
- )
259
 
 
 
 
 
260
  prev_btn.click(
261
- lambda *args: navigate_question("prev", *args),
262
  inputs=[current_question_idx, quiz_questions, user_answers],
263
- outputs=[current_question_idx, question_display, answer_choices, answer_status, question_selector]
264
  )
265
-
266
  next_btn.click(
267
- lambda *args: navigate_question("next", *args),
268
  inputs=[current_question_idx, quiz_questions, user_answers],
269
- outputs=[current_question_idx, question_display, answer_choices, answer_status, question_selector]
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, answer_choices, answer_status]
276
  )
277
 
278
- answer_choices.change(
279
- save_answer,
280
- inputs=[answer_choices, current_question_idx, user_answers],
281
- outputs=[user_answers, answer_status]
 
 
 
 
 
282
  )
283
 
284
  submit_btn.click(
285
- submit_quiz,
286
  inputs=[quiz_questions, user_answers],
287
- outputs=[quiz_section, results_section, restart_btn, start_btn, quiz_active, timer_display]
288
  )
289
 
290
- # Timer update using JavaScript
291
- demo.load(
292
- None,
293
- None,
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
  {