lanna_lalala;- commited on
Commit
b178033
·
1 Parent(s): 51caca0

update lesson -alanna

Browse files
Files changed (1) hide show
  1. phase/Student_view/lesson.py +249 -128
phase/Student_view/lesson.py CHANGED
@@ -1,7 +1,11 @@
1
- import streamlit as st
2
- from utils import db as dbapi
3
  import os
 
 
 
 
4
 
 
 
5
 
6
  # --- Load external CSS ---
7
  def load_css(file_name):
@@ -11,6 +15,11 @@ def load_css(file_name):
11
  except FileNotFoundError:
12
  st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.")
13
 
 
 
 
 
 
14
 
15
  # --- Data structure for lessons ---
16
  lessons_by_level = {
@@ -24,12 +33,14 @@ lessons_by_level = {
24
  "locked": False,
25
  "difficulty": "Easy",
26
  "content": "",
27
- "topics": ["What is money? (coins, notes, digital money)",
28
- "History of money in Jamaica (Jamaican coins, notes, and famous historical figures)",
29
- "How money is used in daily life",
30
- "Different types of currencies",
31
- "Recognizing and counting Jamaican coins and notes",
32
- "Play: Money Match","Quiz", "Summary"]
 
 
33
  },
34
  {
35
  "id": 2,
@@ -39,12 +50,14 @@ lessons_by_level = {
39
  "completed": False,
40
  "locked": False,
41
  "difficulty": "Easy",
42
- "topics": ["Jobs people do to earn money",
43
- "Allowances and pocket money",
44
- "Basic needs vs wants",
45
- "Making choices when spending",
46
- "Simple budget for small items",
47
- "Play: Budget Builder","Quiz", "Summary"]
 
 
48
  },
49
  {
50
  "id": 3,
@@ -54,14 +67,16 @@ lessons_by_level = {
54
  "completed": False,
55
  "locked": False,
56
  "difficulty": "Easy",
57
- "topics": ["Why saving is important",
58
- "Where to save (piggy banks, banks)",
59
- "Basic needs vs wants",
60
- "Setting small savings goals",
61
- "Reward of saving (buying a toy, snack, or school supplies)",
62
- "Play Piggy Bank challenge","Quiz", "Summary"]
 
 
63
  },
64
- {
65
  "id": 4,
66
  "title": "Simple Financial Responsibility",
67
  "description": "Learn to distinguish between essential and optional purchases",
@@ -69,11 +84,13 @@ lessons_by_level = {
69
  "completed": False,
70
  "locked": False,
71
  "difficulty": "Easy",
72
- "topics": ["Making smart choices with money",
73
- "Giving and sharing (donations, helping family/friends)",
74
- "Recognizing scams or unsafe spending",
75
- "Introduction to keeping a simple money diary",
76
- "Play Smart Shopper","Quiz", "Summary"]
 
 
77
  }
78
  ],
79
  "intermediate": [
@@ -144,13 +161,87 @@ lessons_by_level = {
144
 
145
  # --- Utility functions ---
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def _db_to_general_lesson_shape(L: dict, sections: list) -> dict:
148
  """Convert a DB lesson+sections into the same dict shape used by general lessons."""
149
  level = (L.get("level") or "beginner").lower()
150
  difficulty = {"beginner": "Easy", "intermediate": "Medium", "advanced": "Hard"}.get(level, "Easy")
151
- topics = [ (s.get("title") or f"Topic {i+1}") for i, s in enumerate(sections) ]
152
 
153
- # pick a duration heuristic if you don’t store it
154
  duration = L.get("duration") or f"{max(6, len(topics)*6)} min"
155
 
156
  return {
@@ -162,39 +253,20 @@ def _db_to_general_lesson_shape(L: dict, sections: list) -> dict:
162
  "locked": False,
163
  "difficulty": difficulty,
164
  "topics": topics,
165
- # keep optional extra fields if you like:
166
- "_db_sections": sections, # stash raw sections so our DB renderer can use them
167
  "_db_level": level,
168
  }
169
 
170
-
171
  def get_difficulty_color(difficulty):
172
  """Return color based on difficulty level"""
173
- colors = {
174
- "Easy": "#28a745", # Green
175
- "Medium": "#ffc107", # Yellow
176
- "Hard": "#dc3545" # Red
177
- }
178
  return colors.get(difficulty, "#6c757d")
179
 
180
  def get_level_info(level):
181
-
182
  level_info = {
183
- "beginner": {
184
- "display": "🌱 Beginner",
185
- "color": "#28a745",
186
- "description": "Build your financial foundation"
187
- },
188
- "intermediate": {
189
- "display": "🚀 Intermediate",
190
- "color": "#007bff",
191
- "description": "Grow your financial knowledge"
192
- },
193
- "advanced": {
194
- "display": "🎓 Advanced",
195
- "color": "#6f42c1",
196
- "description": "Master advanced strategies"
197
- }
198
  }
199
  return level_info.get(level, level_info["beginner"])
200
 
@@ -217,16 +289,13 @@ def _is_lesson_completed(lesson_id: int) -> bool:
217
  def _complete_lesson(lesson):
218
  _ensure_progress_state()
219
  st.session_state.lesson_completed[lesson["id"]] = True
220
- # reflect in the in-memory lessons list too (so the cards show ✓)
221
  lesson["completed"] = True
222
 
223
-
224
  #-----
225
 
226
  def show_lesson_cards(lessons, user_level):
227
-
228
  level_info = get_level_info(user_level)
229
-
230
  # Level header
231
  st.markdown(f"""
232
  <div style="background-color: {level_info['color']}; color: white; padding: 1.5rem; border-radius: 12px; margin-bottom: 2rem;">
@@ -234,32 +303,31 @@ def show_lesson_cards(lessons, user_level):
234
  <p style="margin: 0.5rem 0 0 0; font-size: 1.1rem; opacity: 0.9;">{level_info['description']}</p>
235
  </div>
236
  """, unsafe_allow_html=True)
237
-
238
  # Stats
239
  total_lessons = len(lessons)
240
  completed_lessons = sum(1 for lesson in lessons if lesson["completed"])
241
-
 
242
  col1, col2, col3, col4 = st.columns(4)
243
  with col1:
244
  st.metric("📚 Total Lessons", total_lessons)
245
  with col2:
246
  st.metric("✅ Completed", completed_lessons)
247
  with col3:
248
- st.metric("📈 Progress", f"{int((completed_lessons/total_lessons)*100)}%")
249
  with col4:
250
  available_lessons = sum(1 for lesson in lessons if not lesson["locked"])
251
  st.metric("🔓 Available", available_lessons)
252
-
253
  st.markdown("---")
254
-
255
  # Display lessons in a 3-column grid like Microsoft Learn
256
  cols = st.columns(3)
257
-
258
  for i, lesson in enumerate(lessons):
259
  lesson["completed"] = lesson.get("completed") or _is_lesson_completed(lesson["id"])
260
 
261
  with cols[i % 3]:
262
- # Determine card status and styling
263
  if lesson["locked"]:
264
  icon_html = '🔒'
265
  card_opacity = "0.6"
@@ -269,7 +337,7 @@ def show_lesson_cards(lessons, user_level):
269
  else:
270
  icon_html = '📖'
271
  card_opacity = "1"
272
-
273
  card_html = f"""
274
  <div class="lesson-card" style="
275
  background: white;
@@ -294,10 +362,8 @@ def show_lesson_cards(lessons, user_level):
294
  <div style="color: #0078d4; font-size: 14px; font-weight: 500;">{lesson['duration']}</div>
295
  </div>
296
  """
297
-
298
  st.markdown(card_html, unsafe_allow_html=True)
299
-
300
- # Native Streamlit buttons that actually work
301
  if lesson["locked"]:
302
  st.button("🔒 Locked", key=f"lesson_locked_{lesson['id']}", disabled=True, use_container_width=True)
303
  elif lesson["completed"]:
@@ -309,21 +375,17 @@ def show_lesson_cards(lessons, user_level):
309
  st.session_state.selected_lesson = lesson["id"]
310
  st.rerun()
311
 
312
-
313
-
314
  def show_lesson_detail(lesson):
 
315
  st.markdown(f"""
316
  <h1 style="font-size:2.5rem; margin-bottom:0;">{lesson['title']}</h1>
317
- <p style="color: #666; margin-top:0;">{lesson['duration']} &nbsp;•&nbsp; Module &nbsp;•&nbsp; {len(lesson.get("topics", []))} units</p>
318
  <hr style="margin:1rem 0;"/>
319
  """, unsafe_allow_html=True)
320
 
321
  # Learning objectives section
322
  st.subheader("Learning objectives")
323
- st.markdown("""
324
- <p style="color:#888; margin-bottom:0.5rem;">In this module, you'll</p>
325
- """, unsafe_allow_html=True)
326
-
327
  st.markdown(
328
  "- Focus on: Basic money concepts \n"
329
  "- Saving \n"
@@ -335,18 +397,17 @@ def show_lesson_detail(lesson):
335
  if st.button("▶ Start", key=f"start_{lesson['id']}", type="primary"):
336
  st.session_state.start_lesson = True
337
  st.session_state.current_topic = 1
338
- # reset progress for this lesson only if not previously completed
339
  _ensure_progress_state()
340
  if not _is_lesson_completed(lesson["id"]):
341
  st.session_state.topic_progress[lesson["id"]] = 1
342
  st.rerun()
343
 
344
-
345
  st.markdown("---")
346
 
347
  # Topics list (with clickable links)
348
  st.subheader("Topics")
349
- for i, t in enumerate(lesson.get("topics", []), start=1):
 
350
  st.markdown(f"- [{t}](#topic-{i})")
351
 
352
  st.markdown("---")
@@ -356,8 +417,6 @@ def show_lesson_detail(lesson):
356
  st.session_state.selected_lesson = None
357
  st.rerun()
358
 
359
-
360
-
361
  def load_topic_content(lesson_id, topic_index):
362
  """Load topic content for a specific lesson and topic index"""
363
  file_path = os.path.join("phase", "Student_view", "lessons", f"lesson_{lesson_id}", f"topic_{topic_index}.txt")
@@ -369,7 +428,6 @@ def load_topic_content(lesson_id, topic_index):
369
  return f"⚠️ Error loading topic content: {e}"
370
  return "⚠️ Topic content not available."
371
 
372
-
373
  def show_lesson_page(lesson, lessons):
374
  # Back link
375
  if st.button("⬅ Back to Lessons", key=f"back_top_{lesson['id']}"):
@@ -397,7 +455,12 @@ def show_lesson_page(lesson, lessons):
397
 
398
  # --- LEFT COLUMN: Lesson content ---
399
  with col1:
400
- topics = lesson.get("topics", [])
 
 
 
 
 
401
  if "current_topic" not in st.session_state:
402
  st.session_state.current_topic = 1
403
 
@@ -411,32 +474,25 @@ def show_lesson_page(lesson, lessons):
411
 
412
  st.subheader(f"Topic {st.session_state.current_topic}: {topic_name}")
413
 
414
- # --- Special handling for Play / Quiz / Normal ---
415
  if topic_name.lower().startswith("play"):
416
  st.info("🎮 This is a game-based activity to reinforce learning.")
417
  st.write("👉 Instructions on how to play go here.")
418
  if st.button("▶ Play Game", key=f"play_game_{lesson['id']}_{topic_idx}"):
419
- st.markdown(
420
- '<a href="http://your-game-url.com" target="_blank">Open Game in New Tab</a>',
421
- unsafe_allow_html=True
422
- )
423
 
424
  elif "quiz" in topic_name.lower():
425
- st.info("📝 Time for a quick quiz!")
426
- st.write("👉 Answer the following questions to test your knowledge.")
427
- answer = st.radio("Q1. What is money?", ["Coins", "Bananas", "Shoes"])
428
- if st.button("Submit Answer", key=f"quiz_submit_{lesson['id']}_{topic_idx}"):
429
- if answer == "Coins":
430
- st.success("✅ Correct!")
431
- else:
432
- st.error("❌ Try again.")
433
  else:
434
  content = load_topic_content(lesson["id"], st.session_state.current_topic)
435
- st.markdown(f"""
436
- <div class="topic-content">
437
- {content}
438
- </div>
439
- """, unsafe_allow_html=True)
440
 
441
  # Topic navigation buttons
442
  prev_col, next_col = st.columns([1, 1])
@@ -454,37 +510,29 @@ def show_lesson_page(lesson, lessons):
454
  is_last_topic = (st.session_state.current_topic >= len(topics))
455
 
456
  if not is_last_topic:
457
- # Normal next
458
  if st.button("Next Topic ➡", key=f"next_topic_{lesson['id']}", type="primary"):
459
  st.session_state.current_topic += 1
460
  st.rerun()
461
  else:
462
- # 🔥 Replace Next with Complete on the last topic
463
  if not _is_lesson_completed(lesson["id"]):
464
  if st.button("✅ Complete Module", key=f"complete_{lesson['id']}", type="primary"):
465
- _complete_lesson(lesson)
466
- st.success("🎉 Module completed! Great job.")
467
- # After completion, bounce back to lessons
468
- st.session_state.selected_lesson = None
469
- st.session_state.current_topic = 1
470
- st.session_state.start_lesson = False
471
  st.rerun()
472
  else:
473
  st.button("✅ Completed", key=f"completed_{lesson['id']}", disabled=True)
474
  st.markdown("</div>", unsafe_allow_html=True)
475
 
476
-
477
  # --- RIGHT COLUMN: Progress + Lesson navigation ---
478
  with col2:
479
  st.subheader("📊 Progress")
480
  _ensure_progress_state()
481
- topics_count = max(1, len(lesson.get("topics", [])))
482
  seen = st.session_state.topic_progress.get(lesson["id"], 0)
483
- # progress is capped at total topics, and if marked complete, force to full
484
- if _is_lesson_completed(lesson["id"]):
485
- pct = 1.0
486
- else:
487
- pct = min(seen / topics_count, 1.0)
488
  st.progress(pct)
489
 
490
  st.subheader("➡ Lesson Navigation")
@@ -502,9 +550,93 @@ def show_lesson_page(lesson, lessons):
502
  st.session_state.current_topic = 1
503
  st.rerun()
504
 
505
-
506
  st.markdown("---")
507
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
  def render_assigned_lesson(lesson_id: int, assignment_id: int | None = None):
510
  """Render a teacher-assigned lesson from the DB using the SAME structure/CSS as general lessons."""
@@ -521,24 +653,19 @@ def render_assigned_lesson(lesson_id: int, assignment_id: int | None = None):
521
 
522
  L = data["lesson"]
523
  sections = sorted(data.get("sections", []), key=lambda s: int(s.get("position", 0)))
524
- # 👉 adapt to the same card/page structure used by general lessons
525
  lesson = _db_to_general_lesson_shape(L, sections)
526
 
527
- # initialization flags (mirror general flow)
528
  if "start_lesson" not in st.session_state:
529
  st.session_state.start_lesson = False
530
  if "selected_lesson" not in st.session_state:
531
  st.session_state.selected_lesson = lesson["id"]
532
 
533
- # Show like general: first a detail screen with Start, then topic pages
534
  if st.session_state.start_lesson:
535
  show_db_lesson_page(lesson)
536
  else:
537
  show_db_lesson_detail(lesson)
538
 
539
-
540
  def show_db_lesson_detail(lesson):
541
- # Same header + objectives block as your general detail view
542
  st.markdown(f"""
543
  <h1 style="font-size:2.5rem; margin-bottom:0;">{lesson['title']}</h1>
544
  <p style="color: #666; margin-top:0;">{lesson['duration']} &nbsp;•&nbsp; Module &nbsp;•&nbsp; {len(lesson.get("topics", []))} units</p>
@@ -573,7 +700,6 @@ def show_db_lesson_detail(lesson):
573
  st.session_state.selected_lesson = None
574
  st.rerun()
575
 
576
-
577
  def show_db_lesson_page(lesson):
578
  # Back link – same as general
579
  if st.button("⬅ Back to Lessons", key=f"db_back_top_{lesson['id']}"):
@@ -613,11 +739,9 @@ def show_db_lesson_page(lesson):
613
 
614
  st.subheader(f"Topic {st.session_state.current_topic}: {topic_name}")
615
 
616
- # Same content container + CSS class as general
617
  body = (sections[idx].get("content") or "⚠️ Topic content not available.")
618
  st.markdown(f"<div class='topic-content'>{body}</div>", unsafe_allow_html=True)
619
 
620
- # Prev/Next / Complete – identical logic
621
  prev_col, next_col = st.columns([1, 1])
622
  with prev_col:
623
  st.markdown("<div class='topic-nav-btn prev-btn'>", unsafe_allow_html=True)
@@ -657,7 +781,6 @@ def show_db_lesson_page(lesson):
657
  st.progress(pct)
658
 
659
  st.subheader("➡ Lesson Navigation")
660
- # For DB lessons, we don’t have a linear list like catalog; just basic controls
661
  if st.button("⬅ Back to Lessons", key=f"db_back_side_{lesson['id']}"):
662
  st.session_state.selected_lesson = None
663
  st.session_state.current_topic = 1
@@ -665,7 +788,6 @@ def show_db_lesson_page(lesson):
665
 
666
  st.markdown("---")
667
 
668
-
669
  def show_page():
670
  # Load CSS
671
  css_path = os.path.join("assets", "styles.css")
@@ -682,8 +804,7 @@ def show_page():
682
  user = st.session_state.user
683
  user_level = user.get("level", "beginner").lower()
684
 
685
- # Deep link from Teacher Link: if a selected_lesson isn't in the catalog,
686
- # render it from the DB instead.
687
  deep_lesson_id = st.session_state.get("selected_lesson")
688
  deep_assignment_id = st.session_state.get("selected_assignment")
689
 
 
 
 
1
  import os
2
+ import re
3
+ import random
4
+ import requests
5
+ from typing import List, Dict
6
 
7
+ import streamlit as st
8
+ from utils import db as dbapi
9
 
10
  # --- Load external CSS ---
11
  def load_css(file_name):
 
15
  except FileNotFoundError:
16
  st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.")
17
 
18
+ # --- Settings ---
19
+ MAX_TOPICS = 5 # show topics 1..5 only, then Summary
20
+ MINI_QUIZ_MIN = 2
21
+ MINI_QUIZ_MAX = 4
22
+ BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") # FastAPI backend (optional)
23
 
24
  # --- Data structure for lessons ---
25
  lessons_by_level = {
 
33
  "locked": False,
34
  "difficulty": "Easy",
35
  "content": "",
36
+ "topics": [
37
+ "What is money? (coins, notes, digital money)",
38
+ "History of money in Jamaica (Jamaican coins, notes, and famous historical figures)",
39
+ "How money is used in daily life",
40
+ "Different types of currencies",
41
+ "Recognizing and counting Jamaican coins and notes",
42
+ "Play: Money Match", "Quiz", "Summary"
43
+ ]
44
  },
45
  {
46
  "id": 2,
 
50
  "completed": False,
51
  "locked": False,
52
  "difficulty": "Easy",
53
+ "topics": [
54
+ "Jobs people do to earn money",
55
+ "Allowances and pocket money",
56
+ "Basic needs vs wants",
57
+ "Making choices when spending",
58
+ "Simple budget for small items",
59
+ "Play: Budget Builder", "Quiz", "Summary"
60
+ ]
61
  },
62
  {
63
  "id": 3,
 
67
  "completed": False,
68
  "locked": False,
69
  "difficulty": "Easy",
70
+ "topics": [
71
+ "Why saving is important",
72
+ "Where to save (piggy banks, banks)",
73
+ "Basic needs vs wants",
74
+ "Setting small savings goals",
75
+ "Reward of saving (buying a toy, snack, or school supplies)",
76
+ "Play Piggy Bank challenge", "Quiz", "Summary"
77
+ ]
78
  },
79
+ {
80
  "id": 4,
81
  "title": "Simple Financial Responsibility",
82
  "description": "Learn to distinguish between essential and optional purchases",
 
84
  "completed": False,
85
  "locked": False,
86
  "difficulty": "Easy",
87
+ "topics": [
88
+ "Making smart choices with money",
89
+ "Giving and sharing (donations, helping family/friends)",
90
+ "Recognizing scams or unsafe spending",
91
+ "Introduction to keeping a simple money diary",
92
+ "Play Smart Shopper", "Quiz", "Summary"
93
+ ]
94
  }
95
  ],
96
  "intermediate": [
 
161
 
162
  # --- Utility functions ---
163
 
164
+ def get_display_topics(lesson: dict) -> List[str]:
165
+ """Limit to topics 1..5 + Summary placeholder, hiding Play/old Quiz."""
166
+ base = (lesson.get("topics") or [])[:MAX_TOPICS]
167
+ return base + ["Summary"]
168
+
169
+ def summarize_first_n_topics(lesson_id: int, n: int = MAX_TOPICS) -> str:
170
+ """Very simple, deterministic summary of the first N topics (no LLM)."""
171
+ bullets = []
172
+ for i in range(1, n + 1):
173
+ text = load_topic_content(lesson_id, i)
174
+ first_sentence = re.split(r'(?<=[.!?])\s+', (text or "").strip())[:1]
175
+ bullets.append(f"• {first_sentence[0] if first_sentence else ('Key idea from topic ' + str(i))}")
176
+ return "<br>".join(bullets) or "No summary available."
177
+
178
+ # ---------- Mini-quiz generation ----------
179
+ _DEFAULT_BANK: List[Dict] = [
180
+ {"id": "b1", "question": "Why do people make a budget?",
181
+ "options": ["To plan money and make choices", "To waste money", "To hide money", "To lose track"],
182
+ "answer_key": "A"},
183
+ {"id": "b2", "question": "A 'need' is best described as:",
184
+ "options": ["A video game", "Shoes for school", "Candy", "Toy car"],
185
+ "answer_key": "B"},
186
+ {"id": "b3", "question": "Saving money means you:",
187
+ "options": ["Keep some for later", "Spend it all now", "Borrow more", "Lose it"],
188
+ "answer_key": "A"},
189
+ {"id": "b4", "question": "Which amount is greater?",
190
+ "options": ["$10", "$5", "$1", "$2"],
191
+ "answer_key": "A"},
192
+ ]
193
+
194
+ def _fallback_auto_quiz() -> List[Dict]:
195
+ k = random.randint(MINI_QUIZ_MIN, MINI_QUIZ_MAX)
196
+ return random.sample(_DEFAULT_BANK, k)
197
+
198
+ def _backend_post(path: str, payload: dict, timeout=6) -> dict | None:
199
+ try:
200
+ r = requests.post(f"{BACKEND_URL}{path}", json=payload, timeout=timeout)
201
+ if r.ok:
202
+ return r.json()
203
+ except Exception:
204
+ pass
205
+ return None
206
+
207
+ def fetch_auto_quiz_from_backend() -> List[Dict] | None:
208
+ # Uses your legacy stub: /agent/quiz returns {"items":[{question,options,answer_key,points}...]}
209
+ resp = _backend_post("/agent/quiz", payload={"lesson_id": 0, "level_slug": "beginner"})
210
+ if resp and isinstance(resp.get("items"), list) and resp["items"]:
211
+ out = []
212
+ for i, it in enumerate(resp["items"], start=1):
213
+ out.append({
214
+ "id": f"auto_{i}",
215
+ "question": it.get("question",""),
216
+ "options": it.get("options", []),
217
+ "answer_key": it.get("answer_key","A")
218
+ })
219
+ return out
220
+ return None
221
+
222
+ def grade_via_backend(answers: List[str]) -> dict | None:
223
+ # /agent/grade expects {"answers":[...]} and returns {"score": int, "total": int}
224
+ return _backend_post("/agent/grade", payload={"answers": answers, "lesson_id": 0, "level_slug": "beginner"})
225
+
226
+ def coach_or_celebrate_via_backend(answers: List[str]) -> dict | None:
227
+ # returns {"route":"coach"/"celebrate", "feedback": "..."} (legacy)
228
+ return _backend_post("/agent/coach_or_celebrate", payload={"answers": answers, "lesson_id": 0, "level_slug": "beginner"})
229
+
230
+ def generate_mini_quiz() -> List[Dict]:
231
+ return fetch_auto_quiz_from_backend() or _fallback_auto_quiz()
232
+
233
+ def grade_locally(items: List[Dict], answers: List[str]) -> tuple[int, int]:
234
+ keys = [it.get("answer_key","A").strip().upper() for it in items]
235
+ raw = (answers or []) + [""]*len(keys)
236
+ score = sum(1 for a, k in zip(raw, keys) if (a or "").strip().upper() == k)
237
+ return score, len(keys)
238
+
239
  def _db_to_general_lesson_shape(L: dict, sections: list) -> dict:
240
  """Convert a DB lesson+sections into the same dict shape used by general lessons."""
241
  level = (L.get("level") or "beginner").lower()
242
  difficulty = {"beginner": "Easy", "intermediate": "Medium", "advanced": "Hard"}.get(level, "Easy")
243
+ topics = [(s.get("title") or f"Topic {i+1}") for i, s in enumerate(sections)]
244
 
 
245
  duration = L.get("duration") or f"{max(6, len(topics)*6)} min"
246
 
247
  return {
 
253
  "locked": False,
254
  "difficulty": difficulty,
255
  "topics": topics,
256
+ "_db_sections": sections,
 
257
  "_db_level": level,
258
  }
259
 
 
260
  def get_difficulty_color(difficulty):
261
  """Return color based on difficulty level"""
262
+ colors = {"Easy": "#28a745", "Medium": "#ffc107", "Hard": "#dc3545"}
 
 
 
 
263
  return colors.get(difficulty, "#6c757d")
264
 
265
  def get_level_info(level):
 
266
  level_info = {
267
+ "beginner": {"display": "🌱 Beginner", "color": "#28a745", "description": "Build your financial foundation"},
268
+ "intermediate": {"display": "🚀 Intermediate", "color": "#007bff", "description": "Grow your financial knowledge"},
269
+ "advanced": {"display": "🎓 Advanced", "color": "#6f42c1", "description": "Master advanced strategies"},
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
  return level_info.get(level, level_info["beginner"])
272
 
 
289
  def _complete_lesson(lesson):
290
  _ensure_progress_state()
291
  st.session_state.lesson_completed[lesson["id"]] = True
 
292
  lesson["completed"] = True
293
 
 
294
  #-----
295
 
296
  def show_lesson_cards(lessons, user_level):
 
297
  level_info = get_level_info(user_level)
298
+
299
  # Level header
300
  st.markdown(f"""
301
  <div style="background-color: {level_info['color']}; color: white; padding: 1.5rem; border-radius: 12px; margin-bottom: 2rem;">
 
303
  <p style="margin: 0.5rem 0 0 0; font-size: 1.1rem; opacity: 0.9;">{level_info['description']}</p>
304
  </div>
305
  """, unsafe_allow_html=True)
306
+
307
  # Stats
308
  total_lessons = len(lessons)
309
  completed_lessons = sum(1 for lesson in lessons if lesson["completed"])
310
+ progress_pct = int((completed_lessons / total_lessons) * 100) if total_lessons else 0
311
+
312
  col1, col2, col3, col4 = st.columns(4)
313
  with col1:
314
  st.metric("📚 Total Lessons", total_lessons)
315
  with col2:
316
  st.metric("✅ Completed", completed_lessons)
317
  with col3:
318
+ st.metric("📈 Progress", f"{progress_pct}%")
319
  with col4:
320
  available_lessons = sum(1 for lesson in lessons if not lesson["locked"])
321
  st.metric("🔓 Available", available_lessons)
322
+
323
  st.markdown("---")
324
+
325
  # Display lessons in a 3-column grid like Microsoft Learn
326
  cols = st.columns(3)
 
327
  for i, lesson in enumerate(lessons):
328
  lesson["completed"] = lesson.get("completed") or _is_lesson_completed(lesson["id"])
329
 
330
  with cols[i % 3]:
 
331
  if lesson["locked"]:
332
  icon_html = '🔒'
333
  card_opacity = "0.6"
 
337
  else:
338
  icon_html = '📖'
339
  card_opacity = "1"
340
+
341
  card_html = f"""
342
  <div class="lesson-card" style="
343
  background: white;
 
362
  <div style="color: #0078d4; font-size: 14px; font-weight: 500;">{lesson['duration']}</div>
363
  </div>
364
  """
 
365
  st.markdown(card_html, unsafe_allow_html=True)
366
+
 
367
  if lesson["locked"]:
368
  st.button("🔒 Locked", key=f"lesson_locked_{lesson['id']}", disabled=True, use_container_width=True)
369
  elif lesson["completed"]:
 
375
  st.session_state.selected_lesson = lesson["id"]
376
  st.rerun()
377
 
 
 
378
  def show_lesson_detail(lesson):
379
+ units = len(get_display_topics(lesson))
380
  st.markdown(f"""
381
  <h1 style="font-size:2.5rem; margin-bottom:0;">{lesson['title']}</h1>
382
+ <p style="color: #666; margin-top:0;">{lesson['duration']} &nbsp;•&nbsp; Module &nbsp;•&nbsp; {units} units</p>
383
  <hr style="margin:1rem 0;"/>
384
  """, unsafe_allow_html=True)
385
 
386
  # Learning objectives section
387
  st.subheader("Learning objectives")
388
+ st.markdown("<p style='color:#888; margin-bottom:0.5rem;'>In this module, you'll</p>", unsafe_allow_html=True)
 
 
 
389
  st.markdown(
390
  "- Focus on: Basic money concepts \n"
391
  "- Saving \n"
 
397
  if st.button("▶ Start", key=f"start_{lesson['id']}", type="primary"):
398
  st.session_state.start_lesson = True
399
  st.session_state.current_topic = 1
 
400
  _ensure_progress_state()
401
  if not _is_lesson_completed(lesson["id"]):
402
  st.session_state.topic_progress[lesson["id"]] = 1
403
  st.rerun()
404
 
 
405
  st.markdown("---")
406
 
407
  # Topics list (with clickable links)
408
  st.subheader("Topics")
409
+ display_topics = get_display_topics(lesson)
410
+ for i, t in enumerate(display_topics, start=1):
411
  st.markdown(f"- [{t}](#topic-{i})")
412
 
413
  st.markdown("---")
 
417
  st.session_state.selected_lesson = None
418
  st.rerun()
419
 
 
 
420
  def load_topic_content(lesson_id, topic_index):
421
  """Load topic content for a specific lesson and topic index"""
422
  file_path = os.path.join("phase", "Student_view", "lessons", f"lesson_{lesson_id}", f"topic_{topic_index}.txt")
 
428
  return f"⚠️ Error loading topic content: {e}"
429
  return "⚠️ Topic content not available."
430
 
 
431
  def show_lesson_page(lesson, lessons):
432
  # Back link
433
  if st.button("⬅ Back to Lessons", key=f"back_top_{lesson['id']}"):
 
455
 
456
  # --- LEFT COLUMN: Lesson content ---
457
  with col1:
458
+ # If the mini-quiz is active, render it and return early
459
+ if st.session_state.get("show_mini_quiz"):
460
+ render_mini_quiz_ui(lesson)
461
+ return
462
+
463
+ topics = get_display_topics(lesson)
464
  if "current_topic" not in st.session_state:
465
  st.session_state.current_topic = 1
466
 
 
474
 
475
  st.subheader(f"Topic {st.session_state.current_topic}: {topic_name}")
476
 
477
+ # --- Special handling for Play / Quiz / Summary / Normal ---
478
  if topic_name.lower().startswith("play"):
479
  st.info("🎮 This is a game-based activity to reinforce learning.")
480
  st.write("👉 Instructions on how to play go here.")
481
  if st.button("▶ Play Game", key=f"play_game_{lesson['id']}_{topic_idx}"):
482
+ st.markdown('<a href="http://your-game-url.com" target="_blank">Open Game in New Tab</a>', unsafe_allow_html=True)
 
 
 
483
 
484
  elif "quiz" in topic_name.lower():
485
+ # kept only for compatibility; normally hidden by get_display_topics
486
+ st.info("📝 Quick quiz (legacy). This is superseded by the auto mini-quiz at completion.")
487
+
488
+ elif topic_name.lower() == "summary":
489
+ st.info("🧾 Module Summary")
490
+ summary_html = summarize_first_n_topics(lesson["id"], MAX_TOPICS)
491
+ st.markdown(f"<div class='topic-content'>{summary_html}</div>", unsafe_allow_html=True)
492
+
493
  else:
494
  content = load_topic_content(lesson["id"], st.session_state.current_topic)
495
+ st.markdown(f"<div class='topic-content'>{content}</div>", unsafe_allow_html=True)
 
 
 
 
496
 
497
  # Topic navigation buttons
498
  prev_col, next_col = st.columns([1, 1])
 
510
  is_last_topic = (st.session_state.current_topic >= len(topics))
511
 
512
  if not is_last_topic:
 
513
  if st.button("Next Topic ➡", key=f"next_topic_{lesson['id']}", type="primary"):
514
  st.session_state.current_topic += 1
515
  st.rerun()
516
  else:
517
+ # Last page = Summary trigger the mini-quiz (2–4 questions)
518
  if not _is_lesson_completed(lesson["id"]):
519
  if st.button("✅ Complete Module", key=f"complete_{lesson['id']}", type="primary"):
520
+ st.session_state.mini_quiz_items = generate_mini_quiz()
521
+ st.session_state.mini_quiz_answers = {}
522
+ st.session_state.mini_q_idx = 0
523
+ st.session_state.show_mini_quiz = True
 
 
524
  st.rerun()
525
  else:
526
  st.button("✅ Completed", key=f"completed_{lesson['id']}", disabled=True)
527
  st.markdown("</div>", unsafe_allow_html=True)
528
 
 
529
  # --- RIGHT COLUMN: Progress + Lesson navigation ---
530
  with col2:
531
  st.subheader("📊 Progress")
532
  _ensure_progress_state()
533
+ topics_count = max(1, len(get_display_topics(lesson)))
534
  seen = st.session_state.topic_progress.get(lesson["id"], 0)
535
+ pct = 1.0 if _is_lesson_completed(lesson["id"]) else min(seen / topics_count, 1.0)
 
 
 
 
536
  st.progress(pct)
537
 
538
  st.subheader("➡ Lesson Navigation")
 
550
  st.session_state.current_topic = 1
551
  st.rerun()
552
 
 
553
  st.markdown("---")
554
+
555
+ def render_mini_quiz_ui(lesson: dict):
556
+ st.markdown("### 📝 Mini Quiz")
557
+ items = st.session_state.get("mini_quiz_items") or []
558
+ if not items:
559
+ st.warning("No questions available.")
560
+ if st.button("Back"):
561
+ st.session_state.show_mini_quiz = False
562
+ st.rerun()
563
+ return
564
+
565
+ idx = int(st.session_state.get("mini_q_idx", 0))
566
+ idx = max(0, min(idx, len(items)))
567
+ st.session_state.mini_q_idx = idx
568
+
569
+ # Not finished → show current question
570
+ if idx < len(items):
571
+ q = items[idx]
572
+ st.subheader(f"Q{idx+1}. {q.get('question','')}")
573
+ opts = q.get("options", []) or []
574
+ key = f"mini_{idx}"
575
+ prev = st.session_state.mini_quiz_answers.get(idx)
576
+ default_index = (opts.index(prev) if (isinstance(prev, str) and prev in opts) else 0) if opts else 0
577
+ choice = st.radio("Select one:", opts or ["(no options)"], index=default_index, key=key)
578
+ st.session_state.mini_quiz_answers[idx] = choice
579
+
580
+ c1, c2 = st.columns(2)
581
+ with c1:
582
+ if idx > 0 and st.button("⬅ Previous"):
583
+ st.session_state.mini_q_idx -= 1
584
+ st.rerun()
585
+ with c2:
586
+ if st.button("Next ➡" if idx < len(items)-1 else "Submit Quiz"):
587
+ if idx < len(items)-1:
588
+ st.session_state.mini_q_idx += 1
589
+ else:
590
+ # grade
591
+ def _letter_for(item, selected_text):
592
+ ops = item.get("options", []) or []
593
+ if selected_text in ops:
594
+ return chr(65 + ops.index(selected_text))
595
+ return ""
596
+
597
+ letters = [_letter_for(items[i], st.session_state.mini_quiz_answers.get(i, "")) for i in range(len(items))]
598
+ backend_score = grade_via_backend(letters)
599
+ if backend_score:
600
+ score, total = backend_score.get("score", 0), backend_score.get("total", len(items))
601
+ else:
602
+ score, total = grade_locally(items, letters)
603
+
604
+ st.session_state.mini_quiz_result = {"score": score, "total": total, "answers": letters}
605
+ st.session_state.mini_q_idx = len(items) # mark finished
606
+ st.rerun()
607
+ return
608
+
609
+ # Finished → results and tutor handoff
610
+ res = st.session_state.get("mini_quiz_result") or {"score": 0, "total": len(items)}
611
+ percent = int(round(100 * res["score"] / max(1, res["total"])))
612
+ st.success(f"🎉 Mini Quiz Completed — Score: {percent}% ({res['score']}/{res['total']})")
613
+
614
+ # Tutor handoff (backend preferred; fallback message)
615
+ tutor = coach_or_celebrate_via_backend(res.get("answers", []))
616
+ if tutor and tutor.get("route") == "coach":
617
+ st.info(f"💬 Tutor: {tutor.get('feedback','Great effort—review the tricky parts and try again!')}")
618
+ elif tutor and tutor.get("route") == "celebrate":
619
+ st.success(f"🏆 Tutor: {tutor.get('message','Excellent work!')}")
620
+ else:
621
+ st.info("💬 Tutor: Nice work! If anything felt tricky, review the Summary above and try a practice game.")
622
+
623
+ colA, colB = st.columns(2)
624
+ with colA:
625
+ if st.button("🔁 Retake Mini Quiz"):
626
+ st.session_state.mini_q_idx = 0
627
+ st.session_state.mini_quiz_answers = {}
628
+ st.session_state.mini_quiz_result = None
629
+ st.rerun()
630
+ with colB:
631
+ if st.button("Finish Module ✅"):
632
+ _complete_lesson(lesson)
633
+ # reset quiz state
634
+ for k in ["show_mini_quiz", "mini_quiz_items", "mini_quiz_answers", "mini_q_idx", "mini_quiz_result"]:
635
+ st.session_state.pop(k, None)
636
+ st.session_state.selected_lesson = None
637
+ st.session_state.current_topic = 1
638
+ st.session_state.start_lesson = False
639
+ st.rerun()
640
 
641
  def render_assigned_lesson(lesson_id: int, assignment_id: int | None = None):
642
  """Render a teacher-assigned lesson from the DB using the SAME structure/CSS as general lessons."""
 
653
 
654
  L = data["lesson"]
655
  sections = sorted(data.get("sections", []), key=lambda s: int(s.get("position", 0)))
 
656
  lesson = _db_to_general_lesson_shape(L, sections)
657
 
 
658
  if "start_lesson" not in st.session_state:
659
  st.session_state.start_lesson = False
660
  if "selected_lesson" not in st.session_state:
661
  st.session_state.selected_lesson = lesson["id"]
662
 
 
663
  if st.session_state.start_lesson:
664
  show_db_lesson_page(lesson)
665
  else:
666
  show_db_lesson_detail(lesson)
667
 
 
668
  def show_db_lesson_detail(lesson):
 
669
  st.markdown(f"""
670
  <h1 style="font-size:2.5rem; margin-bottom:0;">{lesson['title']}</h1>
671
  <p style="color: #666; margin-top:0;">{lesson['duration']} &nbsp;•&nbsp; Module &nbsp;•&nbsp; {len(lesson.get("topics", []))} units</p>
 
700
  st.session_state.selected_lesson = None
701
  st.rerun()
702
 
 
703
  def show_db_lesson_page(lesson):
704
  # Back link – same as general
705
  if st.button("⬅ Back to Lessons", key=f"db_back_top_{lesson['id']}"):
 
739
 
740
  st.subheader(f"Topic {st.session_state.current_topic}: {topic_name}")
741
 
 
742
  body = (sections[idx].get("content") or "⚠️ Topic content not available.")
743
  st.markdown(f"<div class='topic-content'>{body}</div>", unsafe_allow_html=True)
744
 
 
745
  prev_col, next_col = st.columns([1, 1])
746
  with prev_col:
747
  st.markdown("<div class='topic-nav-btn prev-btn'>", unsafe_allow_html=True)
 
781
  st.progress(pct)
782
 
783
  st.subheader("➡ Lesson Navigation")
 
784
  if st.button("⬅ Back to Lessons", key=f"db_back_side_{lesson['id']}"):
785
  st.session_state.selected_lesson = None
786
  st.session_state.current_topic = 1
 
788
 
789
  st.markdown("---")
790
 
 
791
  def show_page():
792
  # Load CSS
793
  css_path = os.path.join("assets", "styles.css")
 
804
  user = st.session_state.user
805
  user_level = user.get("level", "beginner").lower()
806
 
807
+ # Deep link from Teacher Link: if a selected_lesson isn't in the catalog, render it from the DB instead.
 
808
  deep_lesson_id = st.session_state.get("selected_lesson")
809
  deep_assignment_id = st.session_state.get("selected_assignment")
810