jpruzcuen
commited on
Commit
·
f8d9c9f
1
Parent(s):
0daf932
updated quiz ui
Browse files
app.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
import re
|
| 2 |
import gradio as gr
|
| 3 |
-
from quiz import create_quiz, parse_quiz, start_quiz, submit_and_next, restart_quiz
|
|
|
|
| 4 |
from chatbot import chat
|
| 5 |
from llama_cpp import Llama
|
| 6 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 7 |
|
| 8 |
print("Downloading model...")
|
| 9 |
model_path = hf_hub_download(
|
|
@@ -25,6 +27,7 @@ raw_quiz = create_quiz(llm)
|
|
| 25 |
parsed_quiz = parse_quiz(raw_quiz)
|
| 26 |
|
| 27 |
|
|
|
|
| 28 |
with gr.Blocks(title="TAI: AI Teacher Assistant") as demo:
|
| 29 |
gr.Markdown("""
|
| 30 |
# TAI: Your AI Teacher Assistant
|
|
@@ -41,7 +44,7 @@ with gr.Blocks(title="TAI: AI Teacher Assistant") as demo:
|
|
| 41 |
with gr.Row():
|
| 42 |
|
| 43 |
# Left column: chat
|
| 44 |
-
with gr.Column(scale=
|
| 45 |
chatbot = gr.ChatInterface(
|
| 46 |
fn=lambda message, history: chat(llm, message, history),
|
| 47 |
examples=[
|
|
@@ -53,86 +56,55 @@ with gr.Blocks(title="TAI: AI Teacher Assistant") as demo:
|
|
| 53 |
)
|
| 54 |
|
| 55 |
# Right column: quiz
|
| 56 |
-
with gr.Column(scale=
|
| 57 |
gr.Markdown("## Test Yourself")
|
| 58 |
-
|
| 59 |
-
progress = gr.Markdown(f"**Progress:** 0/0", elem_id="quiz-progress")
|
| 60 |
-
question_md = gr.Markdown("", elem_id="quiz-question")
|
| 61 |
-
options = gr.Radio(choices=[], label="Choose an answer", type="value")
|
| 62 |
-
next_btn = gr.Button("Next")
|
| 63 |
start_btn = gr.Button("Start Quiz", variant="primary")
|
| 64 |
-
restart_btn = gr.Button("Restart Quiz")
|
| 65 |
-
result_md = gr.Markdown("", visible=False)
|
| 66 |
-
|
| 67 |
-
# State holders
|
| 68 |
-
quiz_raw_state = gr.State(raw_quiz) # raw quiz text
|
| 69 |
-
quiz_parsed_state = gr.State(parsed_quiz) # parsed list of questions
|
| 70 |
-
idx_state = gr.State(0) # current index
|
| 71 |
-
score_state = gr.State(0) # current score
|
| 72 |
-
finished_state = gr.State(False) # finished flag
|
| 73 |
-
|
| 74 |
-
# Start Quiz -> show first question (no model call)
|
| 75 |
-
def on_start(quiz_raw, quiz_parsed):
|
| 76 |
-
payload, idx, score = start_quiz(quiz_raw, quiz_parsed)
|
| 77 |
-
# return question_md, options choices, idx, score, progress, result_md_visible
|
| 78 |
-
return payload["question_md"], payload["options"], gr.update(value=idx), gr.update(value=score), payload["progress"], gr.update(visible=False), gr.update(visible=True)
|
| 79 |
-
# Note: last two are for hiding/showing result_md (kept simple)
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
outputs=[question_md]
|
| 85 |
-
)
|
| 86 |
|
| 87 |
-
#
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
outputs=[question_md, options, idx_state, score_state, progress, result_md, finished_state]
|
| 96 |
-
)
|
| 97 |
|
| 98 |
-
#
|
| 99 |
-
|
| 100 |
-
md_or_q, opts, new_idx, new_score, prog, finished = submit_and_next(selected, quiz_parsed, idx, score)
|
| 101 |
-
if finished:
|
| 102 |
-
# show final result
|
| 103 |
-
return md_or_q, [], new_idx, new_score, prog, True
|
| 104 |
-
else:
|
| 105 |
-
return md_or_q, opts, new_idx, new_score, prog, False
|
| 106 |
-
|
| 107 |
-
next_btn.click(
|
| 108 |
-
fn=on_next,
|
| 109 |
-
inputs=[options, quiz_parsed_state, idx_state, score_state],
|
| 110 |
-
outputs=[question_md, options, idx_state, score_state, progress, finished_state]
|
| 111 |
-
)
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
return gr.update(value=current_question_md), gr.update(visible=True), gr.update(visible=False)
|
| 118 |
-
else:
|
| 119 |
-
return gr.update(value=""), gr.update(visible=False), gr.update(visible=True)
|
| 120 |
-
|
| 121 |
-
finished_state.change(
|
| 122 |
-
fn=show_result_if_finished,
|
| 123 |
-
inputs=[finished_state, question_md],
|
| 124 |
-
outputs=[result_md, result_md, options] # show result_md & hide options when finished
|
| 125 |
)
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
inputs=[],
|
| 135 |
-
outputs=[question_md,
|
| 136 |
)
|
| 137 |
|
| 138 |
|
|
|
|
| 1 |
import re
|
| 2 |
import gradio as gr
|
| 3 |
+
#from quiz import create_quiz, parse_quiz, start_quiz, submit_and_next, restart_quiz
|
| 4 |
+
from quiz import *
|
| 5 |
from chatbot import chat
|
| 6 |
from llama_cpp import Llama
|
| 7 |
from huggingface_hub import hf_hub_download
|
| 8 |
+
from functools import partial
|
| 9 |
|
| 10 |
print("Downloading model...")
|
| 11 |
model_path = hf_hub_download(
|
|
|
|
| 27 |
parsed_quiz = parse_quiz(raw_quiz)
|
| 28 |
|
| 29 |
|
| 30 |
+
|
| 31 |
with gr.Blocks(title="TAI: AI Teacher Assistant") as demo:
|
| 32 |
gr.Markdown("""
|
| 33 |
# TAI: Your AI Teacher Assistant
|
|
|
|
| 44 |
with gr.Row():
|
| 45 |
|
| 46 |
# Left column: chat
|
| 47 |
+
with gr.Column(scale=2):
|
| 48 |
chatbot = gr.ChatInterface(
|
| 49 |
fn=lambda message, history: chat(llm, message, history),
|
| 50 |
examples=[
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
# Right column: quiz
|
| 59 |
+
with gr.Column(scale=1):
|
| 60 |
gr.Markdown("## Test Yourself")
|
| 61 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
start_btn = gr.Button("Start Quiz", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
question_md = gr.Markdown("")
|
| 65 |
+
feedback_md = gr.Markdown("")
|
| 66 |
+
progress_md = gr.Markdown("")
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
# Answer buttons
|
| 69 |
+
btn_A = gr.Button("A")
|
| 70 |
+
btn_B = gr.Button("B")
|
| 71 |
+
btn_C = gr.Button("C")
|
| 72 |
+
btn_D = gr.Button("D")
|
| 73 |
+
retry_btn = gr.Button("Retry")
|
| 74 |
|
| 75 |
+
# States
|
| 76 |
+
idx_state = gr.State(0)
|
| 77 |
+
score_state = gr.State(0)
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
# Start quiz
|
| 80 |
+
start_fn = partial(start_quiz, parsed_quiz)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
start_btn.click(
|
| 83 |
+
fn=start_fn,
|
| 84 |
+
inputs=[],
|
| 85 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md, start_btn]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
|
| 88 |
+
# Answer button clicks
|
| 89 |
+
answer_fn_A = partial(answer_question, parsed_quiz, "A")
|
| 90 |
+
answer_fn_B = partial(answer_question, parsed_quiz, "B")
|
| 91 |
+
answer_fn_C = partial(answer_question, parsed_quiz, "C")
|
| 92 |
+
answer_fn_D = partial(answer_question, parsed_quiz, "D")
|
| 93 |
+
|
| 94 |
+
btn_A.click(fn=answer_fn_A, inputs=[idx_state, score_state],
|
| 95 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md])
|
| 96 |
+
btn_B.click(fn=answer_fn_B, inputs=[idx_state, score_state],
|
| 97 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md])
|
| 98 |
+
btn_C.click(fn=answer_fn_C, inputs=[idx_state, score_state],
|
| 99 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md])
|
| 100 |
+
btn_D.click(fn=answer_fn_D, inputs=[idx_state, score_state],
|
| 101 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md])
|
| 102 |
+
|
| 103 |
+
# Retry button
|
| 104 |
+
retry_btn.click(
|
| 105 |
+
fn=retry_quiz,
|
| 106 |
inputs=[],
|
| 107 |
+
outputs=[question_md, idx_state, score_state, feedback_md, progress_md, start_btn]
|
| 108 |
)
|
| 109 |
|
| 110 |
|
quiz.py
CHANGED
|
@@ -68,91 +68,30 @@ def parse_quiz(text):
|
|
| 68 |
|
| 69 |
return parsed
|
| 70 |
|
| 71 |
-
|
| 72 |
def start_quiz(quiz_raw, quiz_parsed):
|
| 73 |
-
"""
|
| 74 |
-
# If parse failed or zero questions, show raw text
|
| 75 |
-
if not quiz_parsed:
|
| 76 |
-
return {
|
| 77 |
-
"question_md": "⚠️ Could not parse generated quiz. Showing raw output below:\n\n" + quiz_raw,
|
| 78 |
-
"options": [],
|
| 79 |
-
"progress": "0/0"
|
| 80 |
-
}, 0, 0 # index, score
|
| 81 |
-
|
| 82 |
idx = 0
|
| 83 |
q = quiz_parsed[idx]
|
| 84 |
-
opts = q["options"]
|
| 85 |
-
# If options are empty, present the whole block as markdown
|
| 86 |
-
if not opts:
|
| 87 |
-
return {
|
| 88 |
-
"question_md": f"**Q {idx+1}.** {q['q']}",
|
| 89 |
-
"options": [],
|
| 90 |
-
"progress": f"{idx+1}/{len(quiz_parsed)}"
|
| 91 |
-
}, idx, 0
|
| 92 |
-
|
| 93 |
-
# present options as list of strings
|
| 94 |
return {
|
| 95 |
-
"
|
| 96 |
-
"
|
| 97 |
-
"progress": f"{idx+1}/{len(quiz_parsed)}"
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
- updated index, updated score
|
| 117 |
-
- status/progress string
|
| 118 |
-
"""
|
| 119 |
-
# Validate
|
| 120 |
-
if not quiz_parsed:
|
| 121 |
-
return "No quiz parsed.", [], idx, score, "0/0", False
|
| 122 |
-
|
| 123 |
-
# Check current answer
|
| 124 |
-
if 0 <= idx < len(quiz_parsed):
|
| 125 |
-
current = quiz_parsed[idx]
|
| 126 |
-
correct = current.get("answer")
|
| 127 |
-
# normalize selected like "A) text" -> "A"
|
| 128 |
-
sel_letter = None
|
| 129 |
-
if selected:
|
| 130 |
-
m = re.match(r'\s*([A-D])[\)\.]', selected)
|
| 131 |
-
if m:
|
| 132 |
-
sel_letter = m.group(1).upper()
|
| 133 |
-
# If there is a correct letter available, compare
|
| 134 |
-
if correct and sel_letter:
|
| 135 |
-
if sel_letter == correct:
|
| 136 |
-
score += 1
|
| 137 |
-
|
| 138 |
-
# Move to next question
|
| 139 |
-
idx += 1
|
| 140 |
-
if idx >= len(quiz_parsed):
|
| 141 |
-
# Quiz finished
|
| 142 |
-
result_md = f"### 🧾 Quiz complete!\n\nYour score: **{score} / {len(quiz_parsed)}**"
|
| 143 |
-
# Optionally show correct answers summary
|
| 144 |
-
answers_lines = []
|
| 145 |
-
for i, q in enumerate(quiz_parsed):
|
| 146 |
-
ans = q.get("answer") or "?"
|
| 147 |
-
answers_lines.append(f"{i+1}. {ans}")
|
| 148 |
-
result_md += "\n\n**Correct answers:**\n\n" + "\n".join(answers_lines)
|
| 149 |
-
return result_md, [], idx, score, f"{len(quiz_parsed)}/{len(quiz_parsed)}", True
|
| 150 |
-
|
| 151 |
-
# Otherwise return next question
|
| 152 |
-
md, opts, progress = show_question(quiz_parsed, idx)
|
| 153 |
-
return md, opts, idx, score, progress, False
|
| 154 |
|
| 155 |
-
def restart_quiz(raw_quiz, parsed_quiz):
|
| 156 |
-
"""Reset to the first question, keep the same parsed quiz."""
|
| 157 |
-
payload, idx, score = start_quiz(raw_quiz, parsed_quiz)
|
| 158 |
-
return payload["question_md"], payload["options"], idx, score, payload["progress"]
|
|
|
|
| 68 |
|
| 69 |
return parsed
|
| 70 |
|
|
|
|
| 71 |
def start_quiz(quiz_raw, quiz_parsed):
|
| 72 |
+
"""Initialize quiz state, return first question display."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
idx = 0
|
| 74 |
q = quiz_parsed[idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
return {
|
| 76 |
+
"question_text": q["q"],
|
| 77 |
+
"feedback": "",
|
| 78 |
+
"progress": f"{idx+1}/{len(quiz_parsed)}",
|
| 79 |
+
"show_start": False
|
| 80 |
+
}, idx, 0 # idx, score
|
| 81 |
+
|
| 82 |
+
def answer_question(parsed_quiz, selected, idx, score):
|
| 83 |
+
current = parsed_quiz[idx]
|
| 84 |
+
correct = current["answer"]
|
| 85 |
+
if selected.upper() == correct:
|
| 86 |
+
score += 1
|
| 87 |
+
idx += 1
|
| 88 |
+
if idx >= len(parsed_quiz):
|
| 89 |
+
return "Quiz Complete!", idx, score, "✅ Correct!", f"{len(parsed_quiz)}/{len(parsed_quiz)}"
|
| 90 |
+
q = parsed_quiz[idx]
|
| 91 |
+
return q["q"], idx, score, "✅ Correct!", f"{idx+1}/{len(parsed_quiz)}"
|
| 92 |
+
else:
|
| 93 |
+
return current["q"], idx, score, "❌ Incorrect, try again.", f"{idx+1}/{len(parsed_quiz)}"
|
| 94 |
+
|
| 95 |
+
def retry_quiz():
|
| 96 |
+
return start_quiz()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|