tog's picture
Fix timer and add multi-answer question support
37ab661
import gradio as gr
import json
import random
import time
# Load questions from JSON file
def load_questions():
with open("questionnaire.json", "r") as f:
data = json.load(f)
return data["questions"], data["exam_info"]
QUESTIONS, EXAM_INFO = load_questions()
TIME_LIMIT = 5 * 60 # 5 minutes in seconds
def is_multi_answer(question: dict) -> bool:
return isinstance(question["correct_answer"], list)
def select_random_questions(num_questions: int = 10) -> list:
return random.sample(QUESTIONS, min(num_questions, len(QUESTIONS)))
def format_question(question: dict, index: int) -> str:
suffix = " *(Select all that apply)*" if is_multi_answer(question) else ""
return f"**Question {index + 1}** ({question['section']}){suffix}\n\n{question['question']}"
def calculate_time_remaining(start_time: float) -> tuple[int, int, bool]:
elapsed = time.time() - start_time
remaining = max(0, TIME_LIMIT - elapsed)
minutes = int(remaining // 60)
seconds = int(remaining % 60)
return minutes, seconds, remaining <= 0
def grade_quiz(selected_questions: list, user_answers: list) -> tuple[int, int, list]:
correct_count = 0
results = []
for i, (question, answer) in enumerate(zip(selected_questions, user_answers)):
correct = question["correct_answer"]
if isinstance(correct, int):
correct = [correct]
is_correct = sorted(answer or []) == sorted(correct)
if is_correct:
correct_count += 1
user_ans_text = ", ".join(
f"{chr(65+a)}. {question['options'][a]}" for a in (answer or [])
) or "No answer provided"
correct_ans_text = ", ".join(
f"{chr(65+a)}. {question['options'][a]}" for a in correct
)
results.append({
"question_num": i + 1,
"question": question["question"],
"section": question["section"],
"user_answer_text": user_ans_text,
"correct_answer_text": correct_ans_text,
"is_correct": is_correct,
"explanation": question["explanation"]
})
return correct_count, len(selected_questions), results
def format_results(correct: int, total: int, results: list) -> str:
percentage = (correct / total) * 100
passing = percentage >= EXAM_INFO["passing_score"]
output = "# Quiz Results\n\n"
output += f"## Score: {correct}/{total} ({percentage:.1f}%)\n\n"
output += f"**Status: {'PASSED' if passing else 'FAILED'}** (Passing score: {EXAM_INFO['passing_score']}%)\n\n"
output += "---\n\n"
for result in results:
status_icon = "✅" if result["is_correct"] else "❌"
output += f"### Question {result['question_num']} {status_icon}\n"
output += f"**Section:** {result['section']}\n\n"
output += f"**Question:** {result['question']}\n\n"
output += f"**Your answer:** {result['user_answer_text']}\n\n"
if not result["is_correct"]:
output += f"**Correct answer:** {result['correct_answer_text']}\n\n"
output += f"**Explanation:** {result['explanation']}\n\n"
output += "---\n\n"
return output
def get_answer_components(question: dict, answers: list, q_idx: int):
"""Return (radio_update, checkbox_update) for displaying a question."""
choices = [f"{chr(65+i)}. {opt}" for i, opt in enumerate(question["options"])]
saved = answers[q_idx] or []
if is_multi_answer(question):
prev = [choices[i] for i in saved]
return (
gr.update(choices=choices, value=None, visible=False),
gr.update(choices=choices, value=prev, visible=True),
)
else:
prev = choices[saved[0]] if saved else None
return (
gr.update(choices=choices, value=prev, visible=True),
gr.update(choices=[], value=[], visible=False),
)
# Create Gradio interface
with gr.Blocks(title="NVIDIA Certification Practice Quiz") as demo:
# State variables
quiz_questions = gr.State([])
quiz_start_time = gr.State(0.0)
quiz_active = gr.State(False)
current_question_idx = gr.State(0)
user_answers = gr.State([[] for _ in range(10)])
gr.Markdown(f"""
# {EXAM_INFO['title']}
**Certifications covered:** {', '.join(EXAM_INFO['certifications'])}
**Instructions:**
- You will be presented with 10 randomly selected questions
- You have **5 minutes** to complete the quiz
- Navigate between questions using the Previous/Next buttons or question selector
- Submit your quiz when ready, or it will auto-submit when time expires
- After submission, you'll see your score and explanations for incorrect answers
""")
with gr.Row():
start_btn = gr.Button("Start Quiz", variant="primary")
timer_display = gr.Markdown("**Time Remaining:** 05:00")
with gr.Group(visible=False) as quiz_section:
with gr.Row():
question_selector = gr.Dropdown(
choices=[f"Question {i+1}" for i in range(10)],
value="Question 1",
label="Jump to Question"
)
answer_status = gr.Markdown("**Answered:** 0/10")
question_display = gr.Markdown("", elem_id="question-display")
answer_radio = gr.Radio(choices=[], label="Select your answer:", interactive=True)
answer_checkbox = gr.CheckboxGroup(
choices=[], label="Select all that apply:", interactive=True, visible=False
)
with gr.Row():
prev_btn = gr.Button("◀ Previous")
next_btn = gr.Button("Next ▶")
submit_btn = gr.Button("Submit Quiz", variant="primary")
results_section = gr.Markdown(visible=False)
restart_btn = gr.Button("Start New Quiz", visible=False, variant="primary")
quiz_timer = gr.Timer(value=1)
# --- Event handler functions ---
def start_quiz():
questions = select_random_questions(10)
start_time = time.time()
answers = [[] for _ in range(10)]
question_text = format_question(questions[0], 0)
radio_update, checkbox_update = get_answer_components(questions[0], answers, 0)
return (
questions, # quiz_questions
start_time, # quiz_start_time
True, # quiz_active
0, # current_question_idx
answers, # user_answers
gr.update(visible=True), # quiz_section
question_text, # question_display
radio_update, # answer_radio
checkbox_update, # answer_checkbox
gr.update(visible=False), # results_section
gr.update(visible=False), # restart_btn
gr.update(visible=False), # start_btn
"**Time Remaining:** 05:00", # timer_display
"**Answered:** 0/10", # answer_status
gr.update(value="Question 1"), # question_selector
)
def navigate_question(direction, current_idx, questions, answers):
new_idx = max(0, current_idx - 1) if direction == "prev" else min(9, current_idx + 1)
question = questions[new_idx]
radio_update, checkbox_update = get_answer_components(question, answers, new_idx)
answered_count = sum(1 for a in answers if a)
return (
new_idx,
format_question(question, new_idx),
radio_update,
checkbox_update,
f"**Answered:** {answered_count}/10",
gr.update(value=f"Question {new_idx + 1}"),
)
def jump_to_question(question_label, current_idx, questions, answers):
new_idx = int(question_label.split()[1]) - 1
question = questions[new_idx]
radio_update, checkbox_update = get_answer_components(question, answers, new_idx)
answered_count = sum(1 for a in answers if a)
return (
new_idx,
format_question(question, new_idx),
radio_update,
checkbox_update,
f"**Answered:** {answered_count}/10",
)
def save_answer_radio(answer, current_idx, answers):
answers = answers.copy()
if answer is not None:
answers[current_idx] = [ord(answer[0]) - 65]
answered_count = sum(1 for a in answers if a)
return answers, f"**Answered:** {answered_count}/{len(answers)}"
def save_answer_checkbox(selected, current_idx, answers):
answers = answers.copy()
answers[current_idx] = sorted([ord(s[0]) - 65 for s in selected]) if selected else []
answered_count = sum(1 for a in answers if a)
return answers, f"**Answered:** {answered_count}/{len(answers)}"
def do_submit(questions, answers):
correct, total, results = grade_quiz(questions, answers)
results_text = format_results(correct, total, results)
return (
gr.update(visible=False), # quiz_section
gr.update(value=results_text, visible=True), # results_section
gr.update(visible=True), # restart_btn
gr.update(visible=False), # start_btn
False, # quiz_active
"**Quiz Submitted**", # timer_display
)
def tick_timer(quiz_active_val, start_time_val, questions, answers):
if not quiz_active_val:
return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), quiz_active_val)
minutes, seconds, expired = calculate_time_remaining(start_time_val)
if not expired:
return (
f"**Time Remaining:** {minutes:02d}:{seconds:02d}",
gr.update(), gr.update(), gr.update(), gr.update(),
quiz_active_val,
)
# Auto-submit on expiry
correct, total, results = grade_quiz(questions, answers)
results_text = format_results(correct, total, results)
return (
"**Time Expired**",
gr.update(visible=False),
gr.update(value=results_text, visible=True),
gr.update(visible=True),
gr.update(visible=False),
False,
)
# --- Wire up events ---
start_outputs = [
quiz_questions, quiz_start_time, quiz_active, current_question_idx, user_answers,
quiz_section, question_display, answer_radio, answer_checkbox,
results_section, restart_btn, start_btn, timer_display, answer_status, question_selector,
]
start_btn.click(start_quiz, outputs=start_outputs)
restart_btn.click(start_quiz, outputs=start_outputs)
nav_outputs = [
current_question_idx, question_display, answer_radio, answer_checkbox,
answer_status, question_selector,
]
prev_btn.click(
lambda *a: navigate_question("prev", *a),
inputs=[current_question_idx, quiz_questions, user_answers],
outputs=nav_outputs,
)
next_btn.click(
lambda *a: navigate_question("next", *a),
inputs=[current_question_idx, quiz_questions, user_answers],
outputs=nav_outputs,
)
question_selector.change(
jump_to_question,
inputs=[question_selector, current_question_idx, quiz_questions, user_answers],
outputs=[current_question_idx, question_display, answer_radio, answer_checkbox, answer_status],
)
answer_radio.change(
save_answer_radio,
inputs=[answer_radio, current_question_idx, user_answers],
outputs=[user_answers, answer_status],
)
answer_checkbox.change(
save_answer_checkbox,
inputs=[answer_checkbox, current_question_idx, user_answers],
outputs=[user_answers, answer_status],
)
submit_btn.click(
do_submit,
inputs=[quiz_questions, user_answers],
outputs=[quiz_section, results_section, restart_btn, start_btn, quiz_active, timer_display],
)
quiz_timer.tick(
tick_timer,
inputs=[quiz_active, quiz_start_time, quiz_questions, user_answers],
outputs=[timer_display, quiz_section, results_section, restart_btn, start_btn, quiz_active],
)
if __name__ == "__main__":
demo.launch()