Kerikim commited on
Commit
b677163
·
1 Parent(s): 5f63087

elkay: lesson.py

Browse files
Files changed (1) hide show
  1. phase/Student_view/lesson.py +69 -111
phase/Student_view/lesson.py CHANGED
@@ -1,14 +1,11 @@
 
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
-
8
- # Internal API client (already used across the app)
9
- # Uses BACKEND_URL/BACKEND_TOKEN env vars and has retry logic
10
- # See utils/api.py for details
11
- from utils import api as backend_api
12
 
13
  USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
14
 
@@ -19,14 +16,14 @@ FALLBACK_TAG = "<!--fallback-->"
19
  # ---------------------------------------------
20
  _SS_DEFAULTS = {
21
  "level": "beginner", # beginner | intermediate | advanced
22
- "module_id": None, # int (1-based)
23
- "topic_idx": 0, # 0-based within module
24
  "mode": "catalog", # catalog | lesson | quiz | results
25
- "topics_cache": {}, # {(level, module_id): [(title, text), ...]}
26
- "quiz_data": None, # original quiz payload (list[dict])
27
- "quiz_answers": {}, # q_index -> "A"|"B"|"C"|"D"
28
- "quiz_result": None, # backend result dict
29
- "chatbot_feedback": None, # str
30
  }
31
 
32
  def _ensure_state():
@@ -41,8 +38,6 @@ def _fetch_assigned_lesson(lesson_id: int) -> dict:
41
  """
42
  if USE_LOCAL_DB and hasattr(dbapi, "get_lesson"):
43
  return dbapi.get_lesson(lesson_id) or {}
44
-
45
- # If your backend exposes GET /lessons/{id}
46
  try:
47
  return backend_api.get_lesson(lesson_id) or {}
48
  except Exception:
@@ -51,7 +46,6 @@ def _fetch_assigned_lesson(lesson_id: int) -> dict:
51
  # ---------------------------------------------
52
  # Content metadata (UI only)
53
  # ---------------------------------------------
54
- # These titles mirror the React version you shared, so the experience feels the same.
55
  MODULES_META: Dict[str, List[Dict[str, Any]]] = {
56
  "beginner": [
57
  {
@@ -171,45 +165,22 @@ def _topic_titles(level: str, module_id: int):
171
  @st.cache_data(show_spinner=False, ttl=300)
172
  def _fetch_topic_from_backend(level: str, module_id: int, topic_idx: int) -> Tuple[str, str]:
173
  """
174
- Returns (ui_title, content). Tries backend first, never crashes the UI.
175
- Backend expects folders like: /app/lessons/lesson_{module_id}/topic_{ordinal}.txt
176
  """
177
  plan = _topic_plan(level, module_id)
178
- ui_title, backend_ordinal = plan[topic_idx] # 1-based in original list
179
 
180
- payload = {
181
- "lesson": f"lesson_{module_id}", # folder name
182
- "module": str(module_id), # kept for compatibility
183
- "topic": str(backend_ordinal), # 1-based ordinal mapping
184
- }
185
-
186
- data = {}
187
  try:
188
- # Try your documented route, then a simpler alias
189
- data = backend_api._try_candidates(
190
- "POST",
191
- [
192
- ("/agents/lesson", {"json": payload}),
193
- ("/lesson", {"json": payload}),
194
- ],
195
- ) or {}
196
  except Exception as e:
197
- # Log once, keep the UI moving
198
  st.warning(f"Lesson fetch failed for lesson_{module_id}/topic_{backend_ordinal}: {e}")
199
- data = {}
200
-
201
- # Accept several possible shapes
202
- content = ""
203
- for k in ("lesson_content", "content", "text", "body"):
204
- v = data.get(k)
205
- if isinstance(v, str) and v.strip():
206
- content = v.strip()
207
- break
208
- if isinstance(v, dict):
209
- vv = v.get("content") or v.get("text") or v.get("body")
210
- if isinstance(vv, str) and vv.strip():
211
- content = vv.strip()
212
- break
213
 
214
  return ui_title, content
215
 
@@ -227,7 +198,7 @@ def _extract_takeaways(text: str, max_items: int = 5) -> List[str]:
227
  if items:
228
  return items
229
 
230
- # Otherwise, harvest bullet-y looking lines
231
  bullets = [
232
  ln.strip(" •-*–\t")
233
  for ln in text.splitlines()
@@ -245,8 +216,8 @@ def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
245
  module_conf = next(m for m in MODULES_META[level] if m["id"] == module_id)
246
  try:
247
  quiz = backend_api.generate_quiz(
248
- lesson_id=module_id, # int id works; backend uses it for retrieval bucketing
249
- level_slug=level, # "beginner" | "intermediate" | "advanced"
250
  lesson_title=module_conf["title"],
251
  )
252
  if isinstance(quiz, list) and quiz:
@@ -260,13 +231,17 @@ def _submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]]
260
  """Submit answers and get score + tutor feedback."""
261
  user_answers = []
262
  for i, q in enumerate(original_quiz):
263
- # Expect letters A-D; default to ""
264
  user_answers.append({
265
  "question": q.get("question", f"Q{i+1}"),
266
- "answer": answers_map.get(i, ""),
267
  })
 
 
 
 
268
  try:
269
  result = backend_api.submit_quiz(
 
270
  lesson_id=module_id,
271
  level_slug=level,
272
  user_answers=user_answers,
@@ -299,16 +274,17 @@ def _send_quiz_summary_to_chatbot(result: Dict[str, Any]):
299
  )
300
 
301
  try:
302
- # Hit your FastAPI chatbot route (HF under the hood)
303
- resp = backend_api.send_to_chatbot([
304
- {"role": "system", "content": "You are a friendly financial tutor for Jamaican students."},
305
- {"role": "user", "content": user_prompt}
306
- ])
307
- bot_reply = (resp or {}).get("reply", "").strip()
308
- except Exception as e:
 
309
  bot_reply = f"(Chatbot unavailable) Based on your result: {feedback or 'Nice work!'}"
310
 
311
- # Seed the Chatbot page's message list so the conversation is visible immediately
312
  msgs = st.session_state.get("messages") or [{
313
  "id": "1",
314
  "text": "Hi! I'm your AI Financial Tutor. What would you like to learn today?",
@@ -318,8 +294,6 @@ def _send_quiz_summary_to_chatbot(result: Dict[str, Any]):
318
  msgs.append({"text": user_prompt, "sender": "user", "timestamp": datetime.datetime.now()})
319
  msgs.append({"text": bot_reply, "sender": "assistant", "timestamp": datetime.datetime.now()})
320
  st.session_state.messages = msgs
321
-
322
- # Jump straight to Chatbot page
323
  st.session_state.current_page = "Chatbot"
324
 
325
  def _fallback_text(title: str, module_id: int, topic_ordinal: int) -> str:
@@ -373,18 +347,41 @@ def _render_catalog():
373
  for t, _ord in _topic_plan(level, mod["id"]):
374
  st.write("• ", t)
375
  if st.button("Start Learning", key=f"start_{level}_{mod['id']}"):
376
- # nuke stale topic cache for this module and any cached fetch failures
377
  st.session_state.topics_cache.pop((level, mod["id"]), None)
378
  try:
379
  st.cache_data.clear()
380
  except Exception:
381
  pass
382
-
383
  st.session_state.module_id = mod["id"]
384
  st.session_state.topic_idx = 0
385
  st.session_state.mode = "lesson"
386
  st.rerun()
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  def _render_lesson():
389
  level = st.session_state.level
390
  module_id = st.session_state.module_id
@@ -401,7 +398,6 @@ def _render_lesson():
401
  topics = _get_topics(level, module_id)
402
 
403
  if not topics:
404
- # build topics from metadata and use fallbacks so the page is never blank
405
  plan = _topic_plan(level, module_id)
406
  topics = [(title, _fallback_text(title, module_id, i + 1)) for i, (title, _) in enumerate(plan)]
407
  st.session_state.topics_cache[(level, module_id)] = topics
@@ -429,7 +425,6 @@ def _render_lesson():
429
  source_note = "Default" if FALLBACK_TAG in t_text else "Backend"
430
  st.caption(f"Source: {source_note}")
431
  if t_text:
432
- # strip the marker and render HTML from backend files (e.g., <b>Money</b>)
433
  cleaned = t_text.replace(FALLBACK_TAG, "")
434
  st.markdown(cleaned, unsafe_allow_html=True)
435
 
@@ -455,7 +450,6 @@ def _render_lesson():
455
  is_last = st.session_state.topic_idx >= len(topics) - 1
456
 
457
  if is_last:
458
- # NEW: auto-start quiz when learner reaches the last topic
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…"):
@@ -468,7 +462,6 @@ def _render_lesson():
468
  else:
469
  st.error("Quiz could not be generated. Please try again.")
470
  else:
471
- # Fallback button if auto-start once failed
472
  if st.button("Take Lesson Quiz →"):
473
  with st.spinner("Generating quiz…"):
474
  quiz = _start_quiz(level, module_id)
@@ -489,30 +482,6 @@ def _render_lesson():
489
  label = f"{i+1}. {tt}"
490
  st.button(label, key=f"jump_{i}", on_click=lambda j=i: st.session_state.update({"topic_idx": j}) or st.rerun())
491
 
492
- def _get_topics(level: str, module_id: int) -> List[Tuple[str, str]]:
493
- """
494
- Build the six-topic plan from metadata titles, try backend for each,
495
- and if content is missing, provide a short fallback paragraph.
496
- """
497
- cache_key = (level, module_id)
498
- if cache_key in st.session_state.topics_cache:
499
- return st.session_state.topics_cache[cache_key]
500
-
501
- plan = _topic_plan(level, module_id) # six titles max
502
- out: List[Tuple[str, str]] = []
503
-
504
- for idx in range(len(plan)):
505
- title, content = _fetch_topic_from_backend(level, module_id, idx)
506
- if not content:
507
- content = _fallback_text(title, module_id, idx + 1)
508
- out.append((title, content))
509
-
510
- st.session_state.topics_cache[cache_key] = out
511
- return out
512
-
513
- def _letter_for(i: int) -> str:
514
- return chr(ord("A") + i)
515
-
516
  def _render_quiz():
517
  quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
518
  if not quiz:
@@ -521,26 +490,24 @@ def _render_quiz():
521
 
522
  st.markdown("### Lesson Quiz")
523
 
524
- # Render each question as a block (single page quiz)
525
  for q_idx, q in enumerate(quiz):
526
  st.markdown(f"**Q{q_idx+1}. {q.get('question','').strip()}**")
527
  opts = q.get("options") or []
528
- # Build labels like "A. option"
529
- labels = [f"{_letter_for(i)}. {opt}" for i, opt in enumerate(opts)]
530
 
531
  def _on_select():
532
  sel = st.session_state[f"ans_{q_idx}"] # "A. option text"
533
  letter = sel.split(".", 1)[0] if isinstance(sel, str) else ""
534
- st.session_state.quiz_answers[q_idx] = letter # store "A".."D"
535
 
536
- # Preselect previously chosen letter, if any
537
- saved_letter = st.session_state.quiz_answers.get(q_idx) # "A"
538
  pre_idx = next((i for i, l in enumerate(labels) if saved_letter and l.startswith(f"{saved_letter}.")), None)
539
 
540
  st.radio(
541
  "",
542
  labels,
543
- index=pre_idx, # None means no default
544
  key=f"ans_{q_idx}",
545
  on_change=_on_select,
546
  )
@@ -558,7 +525,6 @@ def _render_quiz():
558
  if result:
559
  st.session_state.quiz_result = result
560
  st.session_state.chatbot_feedback = result.get("feedback")
561
- # NEW: immediately send to chatbot and navigate there
562
  _send_quiz_summary_to_chatbot(result)
563
  st.rerun()
564
 
@@ -618,7 +584,6 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
618
  if not data or not data.get("lesson"):
619
  st.error("Lesson not found or not available.")
620
  if st.button("⬅ Back to Classes"):
621
- # Clear the route and bounce back to the Teacher Link page
622
  st.session_state.lesson_route = None
623
  st.session_state.current_page = "Teacher Link"
624
  st.rerun()
@@ -631,7 +596,6 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
631
  if L.get("description"):
632
  st.caption(L["description"])
633
 
634
- # Track which section we’re on
635
  key = f"_dbsec_idx_{lesson_id}"
636
  if key not in st.session_state:
637
  st.session_state[key] = 0
@@ -670,9 +634,6 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
670
  st.rerun()
671
  with col3:
672
  is_last = idx >= len(sections) - 1
673
-
674
- # If your DB has a quiz section type, you can detect and branch here.
675
- # For now we just advance or finish.
676
  if is_last:
677
  if st.button("Finish"):
678
  st.success("Lesson complete.")
@@ -686,13 +647,11 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
686
 
687
  def render():
688
  _ensure_state()
689
- # If we were routed here from the Teacher Link, skip the catalog flow.
690
  route = st.session_state.get("lesson_route")
691
  if route and route.get("source") == "teacher":
692
  _render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id"))
693
  return
694
 
695
- # Breadcrumb
696
  st.caption("Learning Path · " + st.session_state.level.capitalize())
697
 
698
  mode = st.session_state.mode
@@ -708,10 +667,9 @@ def render():
708
  st.session_state.mode = "catalog"
709
  _render_catalog()
710
 
711
- # Some parts of the app import pages and call a conventional `show()`
712
  show_page = render
713
 
714
  if __name__ == "__main__":
715
- # Allow standalone run for local testing
716
  st.set_page_config(page_title="Lesson", page_icon="📘", layout="centered")
717
- render()
 
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
 
 
16
  # ---------------------------------------------
17
  _SS_DEFAULTS = {
18
  "level": "beginner", # beginner | intermediate | advanced
19
+ "module_id": None, # int (1-based)
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():
 
38
  """
39
  if USE_LOCAL_DB and hasattr(dbapi, "get_lesson"):
40
  return dbapi.get_lesson(lesson_id) or {}
 
 
41
  try:
42
  return backend_api.get_lesson(lesson_id) or {}
43
  except Exception:
 
46
  # ---------------------------------------------
47
  # Content metadata (UI only)
48
  # ---------------------------------------------
 
49
  MODULES_META: Dict[str, List[Dict[str, Any]]] = {
50
  "beginner": [
51
  {
 
165
  @st.cache_data(show_spinner=False, ttl=300)
166
  def _fetch_topic_from_backend(level: str, module_id: int, topic_idx: int) -> Tuple[str, str]:
167
  """
168
+ Returns (ui_title, content). Calls the FastAPI /lesson endpoint via utils.api.fetch_lesson_content.
169
+ The backend expects files at /app/lessons/lesson_{module_id}/topic_{ordinal}.txt
170
  """
171
  plan = _topic_plan(level, module_id)
172
+ ui_title, backend_ordinal = plan[topic_idx] # 1-based ordinal in original list
173
 
 
 
 
 
 
 
 
174
  try:
175
+ content = backend_api.fetch_lesson_content(
176
+ lesson=f"lesson_{module_id}",
177
+ module=str(module_id),
178
+ topic=str(backend_ordinal),
179
+ )
180
+ content = (content or "").strip()
 
 
181
  except Exception as e:
 
182
  st.warning(f"Lesson fetch failed for lesson_{module_id}/topic_{backend_ordinal}: {e}")
183
+ content = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  return ui_title, content
186
 
 
198
  if items:
199
  return items
200
 
201
+ # Otherwise, bullet-ish lines
202
  bullets = [
203
  ln.strip(" •-*–\t")
204
  for ln in text.splitlines()
 
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:
 
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,
 
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?",
 
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:
 
347
  for t, _ord in _topic_plan(level, mod["id"]):
348
  st.write("• ", t)
349
  if st.button("Start Learning", key=f"start_{level}_{mod['id']}"):
350
+ # clear topic cache and cached fetches for a fresh run
351
  st.session_state.topics_cache.pop((level, mod["id"]), None)
352
  try:
353
  st.cache_data.clear()
354
  except Exception:
355
  pass
 
356
  st.session_state.module_id = mod["id"]
357
  st.session_state.topic_idx = 0
358
  st.session_state.mode = "lesson"
359
  st.rerun()
360
 
361
+ def _get_topics(level: str, module_id: int) -> List[Tuple[str, str]]:
362
+ """
363
+ Build the six-topic plan from metadata titles, try backend for each,
364
+ and if content is missing, provide a short fallback paragraph.
365
+ """
366
+ cache_key = (level, module_id)
367
+ if cache_key in st.session_state.topics_cache:
368
+ return st.session_state.topics_cache[cache_key]
369
+
370
+ plan = _topic_plan(level, module_id)
371
+ out: List[Tuple[str, str]] = []
372
+
373
+ for idx in range(len(plan)):
374
+ title, content = _fetch_topic_from_backend(level, module_id, idx)
375
+ if not content:
376
+ content = _fallback_text(title, module_id, idx + 1)
377
+ out.append((title, content))
378
+
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
 
398
  topics = _get_topics(level, module_id)
399
 
400
  if not topics:
 
401
  plan = _topic_plan(level, module_id)
402
  topics = [(title, _fallback_text(title, module_id, i + 1)) for i, (title, _) in enumerate(plan)]
403
  st.session_state.topics_cache[(level, module_id)] = topics
 
425
  source_note = "Default" if FALLBACK_TAG in t_text else "Backend"
426
  st.caption(f"Source: {source_note}")
427
  if t_text:
 
428
  cleaned = t_text.replace(FALLBACK_TAG, "")
429
  st.markdown(cleaned, unsafe_allow_html=True)
430
 
 
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…"):
 
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)
 
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:
 
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
  )
 
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
 
 
584
  if not data or not data.get("lesson"):
585
  st.error("Lesson not found or not available.")
586
  if st.button("⬅ Back to Classes"):
 
587
  st.session_state.lesson_route = None
588
  st.session_state.current_page = "Teacher Link"
589
  st.rerun()
 
596
  if L.get("description"):
597
  st.caption(L["description"])
598
 
 
599
  key = f"_dbsec_idx_{lesson_id}"
600
  if key not in st.session_state:
601
  st.session_state[key] = 0
 
634
  st.rerun()
635
  with col3:
636
  is_last = idx >= len(sections) - 1
 
 
 
637
  if is_last:
638
  if st.button("Finish"):
639
  st.success("Lesson complete.")
 
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"))
653
  return
654
 
 
655
  st.caption("Learning Path · " + st.session_state.level.capitalize())
656
 
657
  mode = st.session_state.mode
 
667
  st.session_state.mode = "catalog"
668
  _render_catalog()
669
 
670
+ # For pages that import and call `show_page()`
671
  show_page = render
672
 
673
  if __name__ == "__main__":
 
674
  st.set_page_config(page_title="Lesson", page_icon="📘", layout="centered")
675
+ render()