Spaces:
Sleeping
Sleeping
Update services/quiz_service.py
Browse files- services/quiz_service.py +96 -127
services/quiz_service.py
CHANGED
|
@@ -104,150 +104,119 @@ def _validate_questions(questions: list[dict]) -> list[dict]:
|
|
| 104 |
|
| 105 |
def _render_quiz_html(questions: list[dict], title: str) -> str:
|
| 106 |
"""
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"""
|
| 112 |
-
|
| 113 |
|
| 114 |
-
css = f"""
|
| 115 |
-
|
| 116 |
-
.{
|
| 117 |
-
font-
|
| 118 |
-
|
| 119 |
-
}}
|
| 120 |
-
.{quiz_id}-title {{
|
| 121 |
-
font-size: 1.05rem;
|
| 122 |
-
font-weight: 700;
|
| 123 |
-
color: #e0e0f0;
|
| 124 |
-
margin-bottom: 20px;
|
| 125 |
-
padding-bottom: 10px;
|
| 126 |
border-bottom: 1px solid rgba(255,255,255,0.08);
|
| 127 |
}}
|
| 128 |
-
.{
|
| 129 |
background: rgba(255,255,255,0.03);
|
| 130 |
border: 1px solid rgba(255,255,255,0.08);
|
| 131 |
-
border-radius: 12px;
|
| 132 |
-
padding: 18px 20px;
|
| 133 |
-
margin-bottom: 14px;
|
| 134 |
-
}}
|
| 135 |
-
.{quiz_id}-qnum {{
|
| 136 |
-
font-size: 0.72rem;
|
| 137 |
-
color: #606080;
|
| 138 |
-
text-transform: uppercase;
|
| 139 |
-
letter-spacing: 0.05em;
|
| 140 |
-
margin-bottom: 6px;
|
| 141 |
}}
|
| 142 |
-
.{
|
| 143 |
-
font-size: 0.
|
| 144 |
-
|
| 145 |
-
color: #d0d0e8;
|
| 146 |
-
margin-bottom: 14px;
|
| 147 |
-
line-height: 1.5;
|
| 148 |
}}
|
| 149 |
-
.{
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
gap: 7px;
|
| 153 |
-
margin-bottom: 12px;
|
| 154 |
}}
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
background: rgba(255,255,255,0.02);
|
| 158 |
-
border: 1px solid rgba(255,255,255,0.
|
| 159 |
-
border-radius: 8px;
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
line-height: 1.4;
|
| 163 |
-
}}
|
| 164 |
-
.{quiz_id}-toggle {{
|
| 165 |
-
display: none;
|
| 166 |
-
}}
|
| 167 |
-
.{quiz_id}-reveal-btn {{
|
| 168 |
-
display: inline-block;
|
| 169 |
-
padding: 7px 18px;
|
| 170 |
-
background: rgba(102,126,234,0.10);
|
| 171 |
-
border: 1px solid rgba(102,126,234,0.25);
|
| 172 |
-
border-radius: 20px;
|
| 173 |
-
font-size: 0.82rem;
|
| 174 |
-
color: #a5b4fc;
|
| 175 |
-
cursor: pointer;
|
| 176 |
-
user-select: none;
|
| 177 |
-
transition: background 0.15s;
|
| 178 |
-
}}
|
| 179 |
-
.{quiz_id}-reveal-btn:hover {{
|
| 180 |
-
background: rgba(102,126,234,0.20);
|
| 181 |
-
}}
|
| 182 |
-
.{quiz_id}-answer {{
|
| 183 |
-
display: none;
|
| 184 |
-
margin-top: 12px;
|
| 185 |
-
padding: 12px 16px;
|
| 186 |
-
background: rgba(34,197,94,0.07);
|
| 187 |
-
border: 1px solid rgba(34,197,94,0.20);
|
| 188 |
-
border-radius: 10px;
|
| 189 |
-
}}
|
| 190 |
-
.{quiz_id}-toggle:checked ~ .{quiz_id}-answer {{
|
| 191 |
-
display: block;
|
| 192 |
-
}}
|
| 193 |
-
.{quiz_id}-toggle:checked ~ label.{quiz_id}-reveal-btn {{
|
| 194 |
-
display: none;
|
| 195 |
-
}}
|
| 196 |
-
.{quiz_id}-toggle:checked ~ .{quiz_id}-options .{quiz_id}-correct-option {{
|
| 197 |
-
border-color: rgba(34,197,94,0.40);
|
| 198 |
-
background: rgba(34,197,94,0.08);
|
| 199 |
-
color: #86efac;
|
| 200 |
-
}}
|
| 201 |
-
.{quiz_id}-correct-label {{
|
| 202 |
-
font-size: 0.82rem;
|
| 203 |
-
color: #86efac;
|
| 204 |
-
font-weight: 700;
|
| 205 |
-
margin-bottom: 5px;
|
| 206 |
-
}}
|
| 207 |
-
.{quiz_id}-explanation {{
|
| 208 |
-
font-size: 0.83rem;
|
| 209 |
-
color: #7090a0;
|
| 210 |
-
line-height: 1.5;
|
| 211 |
}}
|
| 212 |
-
.{
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
}}
|
| 217 |
</style>
|
| 218 |
"""
|
| 219 |
|
| 220 |
-
body = f'<div class="{
|
| 221 |
-
body += f'<div class="{quiz_id}-title">π {title}</div>\n'
|
| 222 |
|
| 223 |
for i, q in enumerate(questions):
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
correct_text = q["options"][
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
body += f'
|
| 230 |
-
body += f' <div class="{
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
body += f' <
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
body += f'</div>\n'
|
| 252 |
|
| 253 |
body += '</div>\n'
|
|
|
|
| 104 |
|
| 105 |
def _render_quiz_html(questions: list[dict], title: str) -> str:
|
| 106 |
"""
|
| 107 |
+
Pure CSS/HTML interactive quiz using radio buttons.
|
| 108 |
+
|
| 109 |
+
Mechanic:
|
| 110 |
+
- Each option is a <label> for a hidden <input type="radio">
|
| 111 |
+
- Clicking an option selects that radio
|
| 112 |
+
- CSS :checked selectors immediately highlight:
|
| 113 |
+
* Selected correct answer β green
|
| 114 |
+
* Selected wrong answer β red + correct answer reveals green
|
| 115 |
+
* Explanation panel appears on any selection
|
| 116 |
+
- No JavaScript needed β works in Gradio gr.HTML sandbox
|
| 117 |
"""
|
| 118 |
+
qid = f"q{uuid.uuid4().hex[:8]}"
|
| 119 |
|
| 120 |
+
css = f"""<style>
|
| 121 |
+
.{qid}-wrap {{ font-family: inherit; max-width: 700px; }}
|
| 122 |
+
.{qid}-title {{
|
| 123 |
+
font-size: 1.05rem; font-weight: 700; color: #e0e0f0;
|
| 124 |
+
margin-bottom: 20px; padding-bottom: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
border-bottom: 1px solid rgba(255,255,255,0.08);
|
| 126 |
}}
|
| 127 |
+
.{qid}-qblock {{
|
| 128 |
background: rgba(255,255,255,0.03);
|
| 129 |
border: 1px solid rgba(255,255,255,0.08);
|
| 130 |
+
border-radius: 12px; padding: 18px 20px; margin-bottom: 14px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
}}
|
| 132 |
+
.{qid}-qnum {{
|
| 133 |
+
font-size: 0.72rem; color: #505068; text-transform: uppercase;
|
| 134 |
+
letter-spacing: 0.05em; margin-bottom: 6px;
|
|
|
|
|
|
|
|
|
|
| 135 |
}}
|
| 136 |
+
.{qid}-qtext {{
|
| 137 |
+
font-size: 0.93rem; font-weight: 600; color: #d0d0e8;
|
| 138 |
+
margin-bottom: 14px; line-height: 1.5;
|
|
|
|
|
|
|
| 139 |
}}
|
| 140 |
+
|
| 141 |
+
/* Hide all radio inputs */
|
| 142 |
+
.{qid}-radio {{ display: none; }}
|
| 143 |
+
|
| 144 |
+
/* Option labels β look like cards */
|
| 145 |
+
.{qid}-opt {{
|
| 146 |
+
display: block; padding: 10px 15px; margin-bottom: 7px;
|
| 147 |
background: rgba(255,255,255,0.02);
|
| 148 |
+
border: 1px solid rgba(255,255,255,0.09);
|
| 149 |
+
border-radius: 8px; font-size: 0.87rem; color: #a0a0c0;
|
| 150 |
+
cursor: pointer; line-height: 1.4;
|
| 151 |
+
transition: border-color 0.12s, background 0.12s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}}
|
| 153 |
+
.{qid}-opt:hover {{ background: rgba(102,126,234,0.07); border-color: rgba(102,126,234,0.25); color: #c0c0e0; }}
|
| 154 |
+
|
| 155 |
+
/* Explanation β hidden until any radio in the group is checked */
|
| 156 |
+
.{qid}-exp {{
|
| 157 |
+
display: none; margin-top: 10px; padding: 11px 15px;
|
| 158 |
+
background: rgba(102,126,234,0.06);
|
| 159 |
+
border-left: 3px solid rgba(102,126,234,0.35);
|
| 160 |
+
border-radius: 0 8px 8px 0;
|
| 161 |
+
font-size: 0.83rem; color: #8888b0; line-height: 1.5;
|
| 162 |
}}
|
| 163 |
</style>
|
| 164 |
"""
|
| 165 |
|
| 166 |
+
body = f'<div class="{qid}-wrap"><div class="{qid}-title">π {title}</div>\n'
|
|
|
|
| 167 |
|
| 168 |
for i, q in enumerate(questions):
|
| 169 |
+
correct = q["answer"] # "A", "B", "C", or "D"
|
| 170 |
+
correct_idx = ord(correct) - 65 # 0-3
|
| 171 |
+
correct_text = q["options"][correct_idx][3:]
|
| 172 |
+
letters = ["A", "B", "C", "D"]
|
| 173 |
+
|
| 174 |
+
body += f'<div class="{qid}-qblock">\n'
|
| 175 |
+
body += f' <div class="{qid}-qnum">Question {i+1} of {len(questions)}</div>\n'
|
| 176 |
+
body += f' <div class="{qid}-qtext">{q["question"]}</div>\n'
|
| 177 |
+
|
| 178 |
+
# Emit all 4 radio inputs FIRST (before labels) so CSS siblings work
|
| 179 |
+
for letter in letters:
|
| 180 |
+
radio_id = f"{qid}_q{i}_{letter}"
|
| 181 |
+
body += f' <input type="radio" class="{qid}-radio" name="{qid}_q{i}" id="{radio_id}" value="{letter}">\n'
|
| 182 |
+
|
| 183 |
+
# Now emit labels (option cards) + CSS rules inline per question
|
| 184 |
+
# We use a <style> block per question to generate the specific selectors
|
| 185 |
+
# because we need to reference the correct letter dynamically
|
| 186 |
+
body += f' <style>\n'
|
| 187 |
+
|
| 188 |
+
for letter in letters:
|
| 189 |
+
radio_id = f"{qid}_q{i}_{letter}"
|
| 190 |
+
correct_radio_id = f"{qid}_q{i}_{correct}"
|
| 191 |
+
|
| 192 |
+
if letter == correct:
|
| 193 |
+
# Selecting the correct answer β green
|
| 194 |
+
body += f' #{radio_id}:checked ~ label[for="{radio_id}"] {{\n'
|
| 195 |
+
body += f' background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.45); color: #86efac;\n'
|
| 196 |
+
body += f' }}\n'
|
| 197 |
+
else:
|
| 198 |
+
# Selecting a wrong answer β that option turns red
|
| 199 |
+
body += f' #{radio_id}:checked ~ label[for="{radio_id}"] {{\n'
|
| 200 |
+
body += f' background: rgba(239,68,68,0.10); border-color: rgba(239,68,68,0.35); color: #fca5a5;\n'
|
| 201 |
+
body += f' }}\n'
|
| 202 |
+
# AND the correct answer reveals green
|
| 203 |
+
body += f' #{radio_id}:checked ~ label[for="{correct_radio_id}"] {{\n'
|
| 204 |
+
body += f' background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.45); color: #86efac;\n'
|
| 205 |
+
body += f' }}\n'
|
| 206 |
+
|
| 207 |
+
# Any selection reveals the explanation
|
| 208 |
+
body += f' #{radio_id}:checked ~ .{qid}-exp {{ display: block; }}\n'
|
| 209 |
+
|
| 210 |
+
body += f' </style>\n'
|
| 211 |
+
|
| 212 |
+
# Option labels
|
| 213 |
+
for letter in letters:
|
| 214 |
+
radio_id = f"{qid}_q{i}_{letter}"
|
| 215 |
+
opt_text = q["options"][ord(letter) - 65]
|
| 216 |
+
body += f' <label for="{radio_id}" class="{qid}-opt">{opt_text}</label>\n'
|
| 217 |
+
|
| 218 |
+
# Explanation (revealed by CSS on any selection)
|
| 219 |
+
body += f' <div class="{qid}-exp">π‘ <strong>Explanation:</strong> {q["explanation"]}</div>\n'
|
| 220 |
body += f'</div>\n'
|
| 221 |
|
| 222 |
body += '</div>\n'
|