lanna_lalala;- commited on
Commit
618422f
·
1 Parent(s): bb9dd1f

miniquiz try

Browse files
phase/Student_view/lesson.py CHANGED
@@ -1,14 +1,22 @@
1
- # phase/Student_view/lesson.py
2
  import streamlit as st
3
  from typing import List, Dict, Any, Optional, Tuple
4
  import re
5
  import datetime
6
  import os
7
  from utils import db as dbapi
8
- from utils import api as backend_api # unified backend client
 
 
 
 
 
 
 
 
9
 
10
  USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
11
 
 
12
  FALLBACK_TAG = "<!--fallback-->"
13
 
14
  # ---------------------------------------------
@@ -20,10 +28,10 @@ _SS_DEFAULTS = {
20
  "topic_idx": 0, # 0-based within module
21
  "mode": "catalog", # catalog | lesson | quiz | results
22
  "topics_cache": {}, # {(level, module_id): [(title, text), ...]}
23
- "quiz_data": None, # original quiz payload (list[dict])
24
- "quiz_answers": {}, # q_index -> "A"|"B"|"C"|"D"
25
- "quiz_result": None, # backend result dict
26
- "chatbot_feedback": None, # str
27
  }
28
 
29
  def _ensure_state():
@@ -211,90 +219,90 @@ def _extract_takeaways(text: str, max_items: int = 5) -> List[str]:
211
  sents = re.split(r"(?<=[.!?])\s+", text.strip())
212
  return [s for s in sents if len(s) > 20][:min(max_items, 3)]
213
 
214
- def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
215
- """Ask backend to generate a 5-question mini quiz for this module."""
216
- module_conf = next(m for m in MODULES_META[level] if m["id"] == module_id)
217
- try:
218
- quiz = backend_api.generate_quiz(
219
- lesson_id=module_id,
220
- level_slug=level,
221
- lesson_title=module_conf["title"],
222
- )
223
- if isinstance(quiz, list) and quiz:
224
- return quiz
225
- return None
226
- except Exception as e:
227
- st.error(f"Could not generate quiz: {e}")
228
- return None
229
-
230
- def _submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]], answers_map: Dict[int, str]) -> Optional[Dict[str, Any]]:
231
- """Submit answers and get score + tutor feedback."""
232
- user_answers = []
233
- for i, q in enumerate(original_quiz):
234
- user_answers.append({
235
- "question": q.get("question", f"Q{i+1}"),
236
- "answer": answers_map.get(i, ""), # "A".."D"
237
- })
238
-
239
- # student_id is required by utils/api.submit_quiz
240
- student_id = int(((st.session_state.get("user") or {}).get("user_id") or 0))
241
-
242
- try:
243
- result = backend_api.submit_quiz(
244
- student_id=student_id,
245
- lesson_id=module_id,
246
- level_slug=level,
247
- user_answers=user_answers,
248
- original_quiz=original_quiz,
249
- )
250
- return result
251
- except Exception as e:
252
- st.error(f"Could not submit quiz: {e}")
253
- return None
254
-
255
- def _send_quiz_summary_to_chatbot(result: Dict[str, Any]):
256
- """
257
- Send a concise, actionable summary of the quiz outcome to the chatbot,
258
- then navigate to the Chatbot page with the conversation pre-seeded.
259
- """
260
- level = st.session_state.level
261
- module_id = st.session_state.module_id
262
- mod = next(m for m in MODULES_META[level] if m["id"] == module_id)
263
-
264
- score = result.get("score", {})
265
- correct = int(score.get("correct", 0))
266
- total = int(score.get("total", 0))
267
- feedback = (result.get("feedback") or st.session_state.get("chatbot_feedback") or "").strip()
268
-
269
- user_prompt = (
270
- f"I just finished the quiz for '{mod['title']}' (module {module_id}) "
271
- f"and scored {correct}/{total}. Please give me 2–3 targeted tips and 1 tiny action "
272
- f"to improve before the next lesson. If there were wrong answers, explain them simply.\n\n"
273
- f"Context from grader:\n{feedback}"
274
- )
275
-
276
- try:
277
- # Call FastAPI /chat via utils.api.chat_ai
278
- bot_reply = (backend_api.chat_ai(
279
- query=user_prompt,
280
- lesson_id=module_id,
281
- level_slug=level,
282
- history=[]
283
- ) or "").strip()
284
- except Exception:
285
- bot_reply = f"(Chatbot unavailable) Based on your result: {feedback or 'Nice work!'}"
286
-
287
- # Seed Chatbot page
288
- msgs = st.session_state.get("messages") or [{
289
- "id": "1",
290
- "text": "Hi! I'm your AI Financial Tutor. What would you like to learn today?",
291
- "sender": "assistant",
292
- "timestamp": datetime.datetime.now(),
293
- }]
294
- msgs.append({"text": user_prompt, "sender": "user", "timestamp": datetime.datetime.now()})
295
- msgs.append({"text": bot_reply, "sender": "assistant", "timestamp": datetime.datetime.now()})
296
- st.session_state.messages = msgs
297
- st.session_state.current_page = "Chatbot"
298
 
299
  def _fallback_text(title: str, module_id: int, topic_ordinal: int) -> str:
300
  """
@@ -379,10 +387,12 @@ def _get_topics(level: str, module_id: int) -> List[Tuple[str, str]]:
379
  st.session_state.topics_cache[cache_key] = out
380
  return out
381
 
382
- def _letter_for(i: int) -> str:
383
- return chr(ord("A") + i)
384
 
385
  def _render_lesson():
 
 
386
  level = st.session_state.level
387
  module_id = st.session_state.module_id
388
  if module_id is None:
@@ -411,11 +421,8 @@ def _render_lesson():
411
  # Special Quiz placeholder
412
  if t_title.strip().lower() == "quiz":
413
  with st.spinner("Generating quiz…"):
414
- quiz = _start_quiz(level, module_id)
415
- if quiz:
416
- st.session_state.quiz_data = quiz
417
- st.session_state.quiz_answers = {}
418
- st.session_state.mode = "quiz"
419
  st.rerun()
420
  else:
421
  st.error("Quiz could not be generated. Please try again or skip.")
@@ -448,27 +455,20 @@ def _render_lesson():
448
  st.rerun()
449
  with col3:
450
  is_last = st.session_state.topic_idx >= len(topics) - 1
451
-
452
  if is_last:
453
  if not st.session_state.get("_auto_quiz_started", False):
454
  st.session_state["_auto_quiz_started"] = True
455
  with st.spinner("Generating quiz…"):
456
- quiz = _start_quiz(level, module_id)
457
- if quiz:
458
- st.session_state.quiz_data = quiz
459
- st.session_state.quiz_answers = {}
460
- st.session_state.mode = "quiz"
461
  st.rerun()
462
  else:
463
  st.error("Quiz could not be generated. Please try again.")
464
  else:
465
  if st.button("Take Lesson Quiz →"):
466
  with st.spinner("Generating quiz…"):
467
- quiz = _start_quiz(level, module_id)
468
- if quiz:
469
- st.session_state.quiz_data = quiz
470
- st.session_state.quiz_answers = {}
471
- st.session_state.mode = "quiz"
472
  st.rerun()
473
  else:
474
  st.error("Quiz could not be generated. Please try again.")
@@ -482,51 +482,52 @@ def _render_lesson():
482
  label = f"{i+1}. {tt}"
483
  st.button(label, key=f"jump_{i}", on_click=lambda j=i: st.session_state.update({"topic_idx": j}) or st.rerun())
484
 
485
- def _render_quiz():
486
- quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
487
- if not quiz:
488
- st.session_state.mode = "lesson"
489
- st.rerun()
490
-
491
- st.markdown("### Lesson Quiz")
492
-
493
- # Render each question (single page)
494
- for q_idx, q in enumerate(quiz):
495
- st.markdown(f"**Q{q_idx+1}. {q.get('question','').strip()}**")
496
- opts = q.get("options") or []
497
 
498
- def _on_select():
499
- sel = st.session_state[f"ans_{q_idx}"] # "A. option text"
500
- letter = sel.split(".", 1)[0] if isinstance(sel, str) else ""
501
- st.session_state.quiz_answers[q_idx] = letter
502
-
503
- labels = [f"{_letter_for(i)}. {opt}" for i, opt in enumerate(opts)]
504
- saved_letter = st.session_state.quiz_answers.get(q_idx)
505
- pre_idx = next((i for i, l in enumerate(labels) if saved_letter and l.startswith(f"{saved_letter}.")), None)
506
-
507
- st.radio(
508
- "",
509
- labels,
510
- index=pre_idx,
511
- key=f"ans_{q_idx}",
512
- on_change=_on_select,
513
- )
514
- st.divider()
515
-
516
- all_answered = len(st.session_state.quiz_answers) == len(quiz)
517
- if st.button("Submit Quiz", disabled=not all_answered):
518
- with st.spinner("Grading…"):
519
- result = _submit_quiz(
520
- st.session_state.level,
521
- st.session_state.module_id,
522
- quiz,
523
- st.session_state.quiz_answers,
524
- )
525
- if result:
526
- st.session_state.quiz_result = result
527
- st.session_state.chatbot_feedback = result.get("feedback")
528
- _send_quiz_summary_to_chatbot(result)
529
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
  def _render_results():
532
  result = st.session_state.quiz_result or {}
@@ -553,10 +554,13 @@ def _render_results():
553
  level = st.session_state.level
554
  module_id = st.session_state.module_id
555
  planned = next((m.get("topics", []) for m in MODULES_META[level] if m["id"] == module_id), [])
556
- try:
557
- quiz_index = [t.strip().lower() for t in planned].index("quiz")
558
- except ValueError:
559
- quiz_index = None
 
 
 
560
 
561
  c1, c2, c3 = st.columns([1, 1, 1])
562
  with c1:
@@ -569,12 +573,12 @@ def _render_results():
569
  st.session_state.current_page = "Chatbot"
570
  st.session_state.chatbot_prefill = fb
571
  st.rerun()
572
- with c3:
573
- if quiz_index is not None and quiz_index + 1 < len(planned):
574
- if st.button("Continue Lesson →"):
575
- st.session_state.mode = "lesson"
576
- st.session_state.topic_idx = quiz_index + 1
577
- st.rerun()
578
 
579
  # ---------------------------------------------
580
  # Public entry point(s)
@@ -647,6 +651,7 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
647
 
648
  def render():
649
  _ensure_state()
 
650
  route = st.session_state.get("lesson_route")
651
  if route and route.get("source") == "teacher":
652
  _render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id"))
@@ -660,13 +665,20 @@ def render():
660
  elif mode == "lesson":
661
  _render_lesson()
662
  elif mode == "quiz":
663
- _render_quiz()
 
 
 
 
 
 
664
  elif mode == "results":
665
  _render_results()
666
  else:
667
  st.session_state.mode = "catalog"
668
  _render_catalog()
669
 
 
670
  # For pages that import and call `show_page()`
671
  show_page = render
672
 
 
 
1
  import streamlit as st
2
  from typing import List, Dict, Any, Optional, Tuple
3
  import re
4
  import datetime
5
  import os
6
  from utils import db as dbapi
7
+ from utils import api as backend_api # unified backend client
8
+
9
+
10
+ # NEW: import the decoupled mini-quiz module (supports both package and flat runs)
11
+ try:
12
+ from .mini_quiz import ensure_quiz_state, start_quiz, render_quiz, render_results as quiz_results
13
+ except Exception:
14
+ from mini_quiz import ensure_quiz_state, start_quiz, render_quiz, render_results as quiz_results
15
+
16
 
17
  USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
18
 
19
+
20
  FALLBACK_TAG = "<!--fallback-->"
21
 
22
  # ---------------------------------------------
 
28
  "topic_idx": 0, # 0-based within module
29
  "mode": "catalog", # catalog | lesson | quiz | results
30
  "topics_cache": {}, # {(level, module_id): [(title, text), ...]}
31
+ # "quiz_data": None, # original quiz payload (list[dict])
32
+ # "quiz_answers": {}, # q_index -> "A"|"B"|"C"|"D"
33
+ # "quiz_result": None, # backend result dict
34
+ # "chatbot_feedback": None, # str
35
  }
36
 
37
  def _ensure_state():
 
219
  sents = re.split(r"(?<=[.!?])\s+", text.strip())
220
  return [s for s in sents if len(s) > 20][:min(max_items, 3)]
221
 
222
+ # def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
223
+ # """Ask backend to generate a 5-question mini quiz for this module."""
224
+ # module_conf = next(m for m in MODULES_META[level] if m["id"] == module_id)
225
+ # try:
226
+ # quiz = backend_api.generate_quiz(
227
+ # lesson_id=module_id,
228
+ # level_slug=level,
229
+ # lesson_title=module_conf["title"],
230
+ # )
231
+ # if isinstance(quiz, list) and quiz:
232
+ # return quiz
233
+ # return None
234
+ # except Exception as e:
235
+ # st.error(f"Could not generate quiz: {e}")
236
+ # return None
237
+
238
+ # def _submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]], answers_map: Dict[int, str]) -> Optional[Dict[str, Any]]:
239
+ # """Submit answers and get score + tutor feedback."""
240
+ # user_answers = []
241
+ # for i, q in enumerate(original_quiz):
242
+ # user_answers.append({
243
+ # "question": q.get("question", f"Q{i+1}"),
244
+ # "answer": answers_map.get(i, ""), # "A".."D"
245
+ # })
246
+
247
+ # # student_id is required by utils/api.submit_quiz
248
+ # student_id = int(((st.session_state.get("user") or {}).get("user_id") or 0))
249
+
250
+ # try:
251
+ # result = backend_api.submit_quiz(
252
+ # student_id=student_id,
253
+ # lesson_id=module_id,
254
+ # level_slug=level,
255
+ # user_answers=user_answers,
256
+ # original_quiz=original_quiz,
257
+ # )
258
+ # return result
259
+ # except Exception as e:
260
+ # st.error(f"Could not submit quiz: {e}")
261
+ # return None
262
+
263
+ # def _send_quiz_summary_to_chatbot(result: Dict[str, Any]):
264
+ # """
265
+ # Send a concise, actionable summary of the quiz outcome to the chatbot,
266
+ # then navigate to the Chatbot page with the conversation pre-seeded.
267
+ # """
268
+ # level = st.session_state.level
269
+ # module_id = st.session_state.module_id
270
+ # mod = next(m for m in MODULES_META[level] if m["id"] == module_id)
271
+
272
+ # score = result.get("score", {})
273
+ # correct = int(score.get("correct", 0))
274
+ # total = int(score.get("total", 0))
275
+ # feedback = (result.get("feedback") or st.session_state.get("chatbot_feedback") or "").strip()
276
+
277
+ # user_prompt = (
278
+ # f"I just finished the quiz for '{mod['title']}' (module {module_id}) "
279
+ # f"and scored {correct}/{total}. Please give me 2–3 targeted tips and 1 tiny action "
280
+ # f"to improve before the next lesson. If there were wrong answers, explain them simply.\n\n"
281
+ # f"Context from grader:\n{feedback}"
282
+ # )
283
+
284
+ # try:
285
+ # # Call FastAPI /chat via utils.api.chat_ai
286
+ # bot_reply = (backend_api.chat_ai(
287
+ # query=user_prompt,
288
+ # lesson_id=module_id,
289
+ # level_slug=level,
290
+ # history=[]
291
+ # ) or "").strip()
292
+ # except Exception:
293
+ # bot_reply = f"(Chatbot unavailable) Based on your result: {feedback or 'Nice work!'}"
294
+
295
+ # # Seed Chatbot page
296
+ # msgs = st.session_state.get("messages") or [{
297
+ # "id": "1",
298
+ # "text": "Hi! I'm your AI Financial Tutor. What would you like to learn today?",
299
+ # "sender": "assistant",
300
+ # "timestamp": datetime.datetime.now(),
301
+ # }]
302
+ # msgs.append({"text": user_prompt, "sender": "user", "timestamp": datetime.datetime.now()})
303
+ # msgs.append({"text": bot_reply, "sender": "assistant", "timestamp": datetime.datetime.now()})
304
+ # st.session_state.messages = msgs
305
+ # st.session_state.current_page = "Chatbot"
306
 
307
  def _fallback_text(title: str, module_id: int, topic_ordinal: int) -> str:
308
  """
 
387
  st.session_state.topics_cache[cache_key] = out
388
  return out
389
 
390
+ # def _letter_for(i: int) -> str:
391
+ # return chr(ord("A") + i)
392
 
393
  def _render_lesson():
394
+ ensure_quiz_state() # make sure quiz keys exist
395
+
396
  level = st.session_state.level
397
  module_id = st.session_state.module_id
398
  if module_id is None:
 
421
  # Special Quiz placeholder
422
  if t_title.strip().lower() == "quiz":
423
  with st.spinner("Generating quiz…"):
424
+ ok = start_quiz(level, module_id, mod["title"])
425
+ if ok:
 
 
 
426
  st.rerun()
427
  else:
428
  st.error("Quiz could not be generated. Please try again or skip.")
 
455
  st.rerun()
456
  with col3:
457
  is_last = st.session_state.topic_idx >= len(topics) - 1
 
458
  if is_last:
459
  if not st.session_state.get("_auto_quiz_started", False):
460
  st.session_state["_auto_quiz_started"] = True
461
  with st.spinner("Generating quiz…"):
462
+ ok = start_quiz(level, module_id, mod["title"])
463
+ if ok:
 
 
 
464
  st.rerun()
465
  else:
466
  st.error("Quiz could not be generated. Please try again.")
467
  else:
468
  if st.button("Take Lesson Quiz →"):
469
  with st.spinner("Generating quiz…"):
470
+ ok = start_quiz(level, module_id, mod["title"])
471
+ if ok:
 
 
 
472
  st.rerun()
473
  else:
474
  st.error("Quiz could not be generated. Please try again.")
 
482
  label = f"{i+1}. {tt}"
483
  st.button(label, key=f"jump_{i}", on_click=lambda j=i: st.session_state.update({"topic_idx": j}) or st.rerun())
484
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
+ # def _render_quiz():
487
+ # quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
488
+ # if not quiz:
489
+ # st.session_state.mode = "lesson"
490
+ # st.rerun()
491
+
492
+ # st.markdown("### Lesson Quiz")
493
+
494
+ # # Render each question (single page)
495
+ # for q_idx, q in enumerate(quiz):
496
+ # st.markdown(f"**Q{q_idx+1}. {q.get('question','').strip()}**")
497
+ # opts = q.get("options") or []
498
+
499
+ # def _on_select():
500
+ # sel = st.session_state[f"ans_{q_idx}"] # "A. option text"
501
+ # letter = sel.split(".", 1)[0] if isinstance(sel, str) else ""
502
+ # st.session_state.quiz_answers[q_idx] = letter
503
+
504
+ # labels = [f"{_letter_for(i)}. {opt}" for i, opt in enumerate(opts)]
505
+ # saved_letter = st.session_state.quiz_answers.get(q_idx)
506
+ # pre_idx = next((i for i, l in enumerate(labels) if saved_letter and l.startswith(f"{saved_letter}.")), None)
507
+
508
+ # st.radio(
509
+ # "",
510
+ # labels,
511
+ # index=pre_idx,
512
+ # key=f"ans_{q_idx}",
513
+ # on_change=_on_select,
514
+ # )
515
+ # st.divider()
516
+
517
+ # all_answered = len(st.session_state.quiz_answers) == len(quiz)
518
+ # if st.button("Submit Quiz", disabled=not all_answered):
519
+ # with st.spinner("Grading…"):
520
+ # result = _submit_quiz(
521
+ # st.session_state.level,
522
+ # st.session_state.module_id,
523
+ # quiz,
524
+ # st.session_state.quiz_answers,
525
+ # )
526
+ # if result:
527
+ # st.session_state.quiz_result = result
528
+ # st.session_state.chatbot_feedback = result.get("feedback")
529
+ # _send_quiz_summary_to_chatbot(result)
530
+ # st.rerun()
531
 
532
  def _render_results():
533
  result = st.session_state.quiz_result or {}
 
554
  level = st.session_state.level
555
  module_id = st.session_state.module_id
556
  planned = next((m.get("topics", []) for m in MODULES_META[level] if m["id"] == module_id), [])
557
+
558
+ # Let mini_quiz handle the full results UI (review, feedback, nav)
559
+ quiz_results(planned_topics=planned)
560
+ # try:
561
+ # quiz_index = [t.strip().lower() for t in planned].index("quiz")
562
+ # except ValueError:
563
+ # quiz_index = None
564
 
565
  c1, c2, c3 = st.columns([1, 1, 1])
566
  with c1:
 
573
  st.session_state.current_page = "Chatbot"
574
  st.session_state.chatbot_prefill = fb
575
  st.rerun()
576
+ # with c3:
577
+ # if quiz_index is not None and quiz_index + 1 < len(planned):
578
+ # if st.button("Continue Lesson →"):
579
+ # st.session_state.mode = "lesson"
580
+ # st.session_state.topic_idx = quiz_index + 1
581
+ # st.rerun()
582
 
583
  # ---------------------------------------------
584
  # Public entry point(s)
 
651
 
652
  def render():
653
  _ensure_state()
654
+ ensure_quiz_state() # ensure quiz keys whenever the page renders
655
  route = st.session_state.get("lesson_route")
656
  if route and route.get("source") == "teacher":
657
  _render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id"))
 
665
  elif mode == "lesson":
666
  _render_lesson()
667
  elif mode == "quiz":
668
+ # Hand off to mini-quiz UI. Pass title for chatbot summary when grading.
669
+ # We can retrieve the module title here for convenience.
670
+ try:
671
+ mod = next(m for m in MODULES_META[st.session_state.level] if m["id"] == st.session_state.module_id)
672
+ render_quiz(lesson_title=mod["title"])
673
+ except Exception:
674
+ render_quiz(lesson_title=None)
675
  elif mode == "results":
676
  _render_results()
677
  else:
678
  st.session_state.mode = "catalog"
679
  _render_catalog()
680
 
681
+
682
  # For pages that import and call `show_page()`
683
  show_page = render
684
 
phase/Student_view/mini_quiz.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # phase/Student_view/mini_quiz.py
2
+ import datetime
3
+ from typing import List, Dict, Any, Optional
4
+ import streamlit as st
5
+
6
+ # Reuse the unified backend client
7
+ from utils import api as backend_api
8
+
9
+ # ---------------------------------------------
10
+ # Public quiz-state helpers
11
+ # ---------------------------------------------
12
+ QUIZ_SS_DEFAULTS = {
13
+ "quiz_data": None, # original quiz payload (list[dict])
14
+ "quiz_answers": {}, # q_index -> "A"|"B"|"C"|"D"
15
+ "quiz_result": None, # backend result dict
16
+ "chatbot_feedback": None, # str
17
+ "_auto_quiz_started": False, # helper to avoid double-start
18
+ }
19
+
20
+
21
+ def ensure_quiz_state():
22
+ """Ensure quiz-related keys exist in st.session_state."""
23
+ for k, v in QUIZ_SS_DEFAULTS.items():
24
+ if k not in st.session_state:
25
+ st.session_state[k] = v
26
+
27
+
28
+ # ---------------------------------------------
29
+ # Backend calls (isolated here)
30
+ # ---------------------------------------------
31
+
32
+ def start_quiz(level: str, module_id: int, lesson_title: str) -> bool:
33
+ """Generate and stage a quiz for the given module. Returns True on success."""
34
+ ensure_quiz_state()
35
+ try:
36
+ quiz = backend_api.generate_quiz(
37
+ lesson_id=module_id,
38
+ level_slug=level,
39
+ lesson_title=lesson_title,
40
+ )
41
+ except Exception as e:
42
+ st.error(f"Could not generate quiz: {e}")
43
+ return False
44
+
45
+ if isinstance(quiz, list) and quiz:
46
+ st.session_state.quiz_data = quiz
47
+ st.session_state.quiz_answers = {}
48
+ st.session_state.mode = "quiz"
49
+ return True
50
+
51
+ st.error("Quiz could not be generated. Please try again.")
52
+ return False
53
+
54
+
55
+ def submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]], answers_map: Dict[int, str]) -> Optional[Dict[str, Any]]:
56
+ """Submit answers and return the grading result dict."""
57
+ user_answers = []
58
+ for i, q in enumerate(original_quiz):
59
+ user_answers.append({
60
+ "question": q.get("question", f"Q{i+1}"),
61
+ "answer": answers_map.get(i, ""), # "A".."D"
62
+ })
63
+
64
+ # student_id is required by utils.api.submit_quiz
65
+ student_id = int(((st.session_state.get("user") or {}).get("user_id") or 0))
66
+
67
+ try:
68
+ result = backend_api.submit_quiz(
69
+ student_id=student_id,
70
+ lesson_id=module_id,
71
+ level_slug=level,
72
+ user_answers=user_answers,
73
+ original_quiz=original_quiz,
74
+ )
75
+ return result
76
+ except Exception as e:
77
+ st.error(f"Could not submit quiz: {e}")
78
+ return None
79
+
80
+
81
+ def send_quiz_summary_to_chatbot(level: str, module_id: int, lesson_title: str, result: Dict[str, Any]):
82
+ """Send a concise summary to the chatbot and navigate there."""
83
+ score = result.get("score", {})
84
+ correct = int(score.get("correct", 0))
85
+ total = int(score.get("total", 0))
86
+ feedback = (result.get("feedback") or st.session_state.get("chatbot_feedback") or "").strip()
87
+
88
+ user_prompt = (
89
+ f"I just finished the quiz for '{lesson_title}' (module {module_id}) "
90
+ f"and scored {correct}/{total}. Please give me 2–3 targeted tips and 1 tiny action "
91
+ f"to improve before the next lesson. If there were wrong answers, explain them simply.\n\n"
92
+ f"Context from grader:\n{feedback}"
93
+ )
94
+
95
+ try:
96
+ bot_reply = (backend_api.chat_ai(
97
+ query=user_prompt,
98
+ lesson_id=module_id,
99
+ level_slug=level,
100
+ history=[],
101
+ ) or "").strip()
102
+ except Exception:
103
+ bot_reply = f"(Chatbot unavailable) Based on your result: {feedback or 'Nice work!'}"
104
+
105
+ # Seed Chatbot page
106
+ msgs = st.session_state.get("messages") or [{
107
+ "id": "1",
108
+ "text": "Hi! I'm your AI Financial Tutor. What would you like to learn today?",
109
+ "sender": "assistant",
110
+ "timestamp": datetime.datetime.now(),
111
+ }]
112
+ msgs.append({"text": user_prompt, "sender": "user", "timestamp": datetime.datetime.now()})
113
+ msgs.append({"text": bot_reply, "sender": "assistant", "timestamp": datetime.datetime.now()})
114
+
115
+ st.session_state.messages = msgs
116
+ st.session_state.current_page = "Chatbot"
117
+
118
+
119
+ # ---------------------------------------------
120
+ # UI helpers
121
+ # ---------------------------------------------
122
+
123
+ def _letter_for(i: int) -> str:
124
+ return chr(ord("A") + i)
125
+
126
+
127
+ def render_quiz(lesson_title: Optional[str] = None):
128
+ """Render the quiz UI (single page). Expects st.session_state.level/module_id to be set."""
129
+ ensure_quiz_state()
130
+
131
+ quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
132
+ if not quiz:
133
+ # No staged quiz — bounce back to lesson
134
+ st.session_state.mode = "lesson"
135
+ st.rerun()
136
+
137
+ st.markdown("### Lesson Quiz")
138
+
139
+ # Render each question
140
+ for q_idx, q in enumerate(quiz):
141
+ st.markdown(f"**Q{q_idx+1}. {q.get('question','').strip()}**")
142
+ opts = q.get("options") or []
143
+
144
+ def _on_select():
145
+ sel = st.session_state[f"ans_{q_idx}"] # e.g. "A. option text"
146
+ letter = sel.split(".", 1)[0] if isinstance(sel, str) else ""
147
+ st.session_state.quiz_answers[q_idx] = letter
148
+
149
+ labels = [f"{_letter_for(i)}. {opt}" for i, opt in enumerate(opts)]
150
+ saved_letter = st.session_state.quiz_answers.get(q_idx)
151
+ pre_idx = next((i for i, l in enumerate(labels) if saved_letter and l.startswith(f"{saved_letter}.")), None)
152
+
153
+ st.radio(
154
+ "",
155
+ labels,
156
+ index=pre_idx,
157
+ key=f"ans_{q_idx}",
158
+ on_change=_on_select,
159
+ )
160
+ st.divider()
161
+
162
+ all_answered = len(st.session_state.quiz_answers) == len(quiz)
163
+ if st.button("Submit Quiz", disabled=not all_answered):
164
+ with st.spinner("Grading…"):
165
+ result = submit_quiz(
166
+ st.session_state.level,
167
+ st.session_state.module_id,
168
+ quiz,
169
+ st.session_state.quiz_answers,
170
+ )
171
+ if result:
172
+ st.session_state.quiz_result = result
173
+ st.session_state.chatbot_feedback = result.get("feedback")
174
+ # Keep current behavior: jump to Chatbot after grading
175
+ send_quiz_summary_to_chatbot(
176
+ st.session_state.level,
177
+ st.session_state.module_id,
178
+ lesson_title or "This Lesson",
179
+ result,
180
+ )
181
+ st.rerun()
182
+
183
+
184
+ def render_results(planned_topics: Optional[List[str]] = None):
185
+ """Optional results screen (not used by default flow which jumps to Chatbot)."""
186
+ ensure_quiz_state()
187
+
188
+ result = st.session_state.quiz_result or {}
189
+ score = result.get("score", {})
190
+ correct = score.get("correct", 0)
191
+ total = score.get("total", 0)
192
+
193
+ st.success(f"Quiz Complete! You scored {correct} / {total}.")
194
+
195
+ wrong = result.get("wrong", [])
196
+ if wrong:
197
+ with st.expander("Review your answers"):
198
+ for w in wrong:
199
+ st.markdown(f"**{w.get('question','')}**")
200
+ st.write(f"Your answer: {w.get('your_answer','')}")
201
+ st.write(f"Correct answer: {w.get('correct_answer','')}")
202
+ st.divider()
203
+
204
+ fb = st.session_state.chatbot_feedback
205
+ if fb:
206
+ st.markdown("#### Tutor Explanation")
207
+ st.write(fb)
208
+
209
+ # Navigation controls
210
+ planned_topics = planned_topics or []
211
+ try:
212
+ quiz_index = [t.strip().lower() for t in planned_topics].index("quiz")
213
+ except ValueError:
214
+ quiz_index = None
215
+
216
+ c1, c2, c3 = st.columns([1, 1, 1])
217
+ with c1:
218
+ if st.button("Back to Modules"):
219
+ st.session_state.mode = "catalog"
220
+ st.session_state.module_id = None
221
+ st.rerun()
222
+ with c2:
223
+ if st.button("Ask the Chatbot →"):
224
+ st.session_state.current_page = "Chatbot"
225
+ st.session_state.chatbot_prefill = fb
226
+ st.rerun()
227
+ with c3:
228
+ if quiz_index is not None and quiz_index + 1 < len(planned_topics):
229
+ if st.button("Continue Lesson →"):
230
+ st.session_state.mode = "lesson"
231
+ st.session_state.topic_idx = quiz_index + 1
232
+ st.rerun()
233
+
234
+
phase/Student_view/practice_quiz.py DELETED
@@ -1 +0,0 @@
1
- #added a practice_quiz.py (for the general practice quiz code could go here and the lesson_quiz code stuff in quiz.py)