| import streamlit as st |
| from typing import List, Dict, Any, Optional, Tuple |
| import re |
| import os |
| from utils import db as dbapi |
| from utils import api as backend_api |
| from phase.Student_view import quiz as quiz_page |
|
|
|
|
|
|
| USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" |
|
|
|
|
| FALLBACK_TAG = "<!--fallback-->" |
|
|
| |
| def load_css(file_name: str): |
| try: |
| with open(file_name, "r", encoding="utf-8") as f: |
| st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True) |
| except FileNotFoundError: |
| st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.") |
| |
| |
| |
| _SS_DEFAULTS = { |
| "level": "beginner", |
| "module_id": None, |
| "topic_idx": 0, |
| "mode": "catalog", |
| "topics_cache": {}, |
| "show_quiz_prompt": False, |
| "selected_quiz": None, |
| } |
|
|
| |
| if hasattr(st, "dialog"): |
| @st.dialog("Ready for a quick check-in?") |
| def _quiz_prompt_dialog(): |
| st.write("Would you like to do **Quiz 1** to strengthen your knowledge?") |
| c1, c2 = st.columns(2) |
| if c1.button("Yes, start Quiz 1", key="dlg_yes"): |
| st.session_state.show_quiz_prompt = False |
| st.session_state.mode = "quiz" |
| st.session_state.selected_quiz = 1 |
| st.session_state.current_q = 0 |
| st.session_state.answers = {} |
| st.rerun() |
| if c2.button("Maybe later", key="dlg_no"): |
| st.session_state.show_quiz_prompt = False |
| st.session_state.mode = "catalog" |
| st.session_state.module_id = None |
| st.session_state.topic_idx = 0 |
| st.rerun() |
| else: |
| |
| def _quiz_prompt_dialog(): |
| st.info("Would you like to do **Quiz 1** to strengthen your knowledge?") |
| c1, c2 = st.columns(2) |
| if c1.button("Yes, start Quiz 1", key="inline_yes"): |
| st.session_state.mode = "quiz" |
| st.session_state.selected_quiz = 1 |
| st.session_state.current_q = 0 |
| st.session_state.answers = {} |
| st.rerun() |
| if c2.button("Maybe later", key="inline_no"): |
| st.session_state.mode = "catalog" |
| st.session_state.module_id = None |
| st.session_state.topic_idx = 0 |
| st.rerun() |
|
|
|
|
| def _ensure_state(): |
| for k, v in _SS_DEFAULTS.items(): |
| if k not in st.session_state: |
| st.session_state[k] = v |
|
|
| def _fetch_assigned_lesson(lesson_id: int) -> dict: |
| """ |
| Returns {"lesson": {...}, "sections": [...]} |
| Falls back to backend_api if you're not using the local DB. |
| """ |
| if USE_LOCAL_DB and hasattr(dbapi, "get_lesson"): |
| return dbapi.get_lesson(lesson_id) or {} |
| try: |
| return backend_api.get_lesson(lesson_id) or {} |
| except Exception: |
| return {} |
|
|
| |
| |
| |
| MODULES_META: Dict[str, List[Dict[str, Any]]] = { |
| "beginner": [ |
| { |
| "id": 1, |
| "title": "Understanding Money", |
| "description": "Learn the basics of what money is, its uses, and how to manage it.", |
| "duration": "20 min", |
| "completed": False, |
| "locked": False, |
| "difficulty": "Easy", |
| "topics": [ |
| "What is Money?", |
| "Needs vs. Wants", |
| "Earning Money", |
| "Saving Money", |
| "Spending Wisely", |
| "Play: Money Match", |
| "Quiz", |
| "Summary: My Money Plan" |
| ] |
| }, |
| { |
| "id": 2, |
| "title": "Basic Budgeting", |
| "description": "Start building the habit of planning and managing money through budgeting.", |
| "duration": "20 min", |
| "completed": False, |
| "locked": False, |
| "difficulty": "Easy", |
| "topics": [ |
| "What is a Budget?", |
| "Income and Expenses", |
| "Profit and Loss", |
| "Saving Goals", |
| "Making Choices", |
| "Play: Budget Builder", |
| "Quiz", |
| "Summary: My First Budget" |
| ] |
| }, |
| { |
| "id": 3, |
| "title": "Money in Action", |
| "description": "Learn how money is used in everyday transactions and its role in society.", |
| "duration": "20 min", |
| "completed": False, |
| "locked": False, |
| "difficulty": "Easy", |
| "topics": [ |
| "Paying for Things", |
| "Keeping Track of Money", |
| "What Are Taxes?", |
| "Giving and Sharing", |
| "Money Safety", |
| "Play: Piggy Bank Challenge", |
| "Quiz", |
| "Summary: Money Journal" |
| ] |
| }, |
| { |
| "id": 4, |
| "title": "Simple Business Ideas", |
| "description": "Explore the basics of starting a small business and earning profit.", |
| "duration": "20 min", |
| "completed": False, |
| "locked": False, |
| "difficulty": "Easy", |
| "topics": [ |
| "What is a Business?", |
| "Costs in a Business", |
| "Revenue in a Business", |
| "Profit in a Business", |
| "Advertising Basics", |
| "Play: Smart Shopper", |
| "Quiz", |
| "Summary: My Business Plan" |
| ] |
| } |
| ], |
| "intermediate": [], |
| "advanced": [], |
| } |
|
|
| def _topic_plan(level: str, module_id: int): |
| """ |
| Returns a list of (title, backend_ordinal) after filtering: |
| - drop any 'Play:' topic |
| - drop 'Quiz' |
| - keep first five + the Summary (6 total) |
| backend_ordinal is the 1-based index in the original metadata (so backend files line up). |
| """ |
| mod = next(m for m in MODULES_META[level] if m["id"] == module_id) |
| raw = (mod.get("topics") or mod.get("topic_labels") or []) |
| plan = [] |
| for i, t in enumerate(raw, start=1): |
| tl = t.strip().lower() |
| if tl == "quiz" or tl.startswith("play:"): |
| continue |
| plan.append((t, i)) |
|
|
| |
| if len(plan) > 6: |
| summary_pos = next((idx for idx, (title, _) in enumerate(plan) |
| if title.strip().lower().startswith("summary")), None) |
| if summary_pos is not None: |
| plan = plan[:5] + [plan[summary_pos]] |
| else: |
| plan = plan[:6] |
| return plan |
|
|
| def _topic_titles(level: str, module_id: int): |
| return [t for (t, _) in _topic_plan(level, module_id)] |
|
|
| |
| |
| |
| @st.cache_data(show_spinner=False, ttl=300) |
| def _fetch_topic_from_backend(level: str, module_id: int, topic_idx: int) -> Tuple[str, str]: |
| """ |
| Returns (ui_title, content). Calls the FastAPI /lesson endpoint via utils.api.fetch_lesson_content. |
| The backend expects files at /app/lessons/lesson_{module_id}/topic_{ordinal}.txt |
| """ |
| plan = _topic_plan(level, module_id) |
| ui_title, backend_ordinal = plan[topic_idx] |
|
|
| try: |
| content = backend_api.fetch_lesson_content( |
| lesson=f"lesson_{module_id}", |
| module=str(module_id), |
| topic=str(backend_ordinal), |
| ) |
| content = (content or "").strip() |
| except Exception as e: |
| st.warning(f"Lesson fetch failed for lesson_{module_id}/topic_{backend_ordinal}: {e}") |
| content = "" |
|
|
| return ui_title, content |
|
|
|
|
|
|
| def _fallback_text(title: str, module_id: int, topic_ordinal: int) -> str: |
| """ |
| Minimal on-brand copy if the backend has not supplied a topic file yet. |
| Tailored to beginner modules so the UI stays useful. |
| """ |
| t = title.strip().lower() |
| if "what is money" in t or "money" == t: |
| return ("Money is a tool we use to trade for goods and services. " |
| "In Jamaica we use JMD coins and notes, and many people also pay digitally. " |
| "You can spend, save, or share money, but it is limited, so plan how you use it.") + "\n" + FALLBACK_TAG |
| if "need" in t and "want" in t: |
| return ("Needs keep you safe and healthy, like food, clothes, and school supplies. " |
| "Wants are nice to have, like toys or snacks. Cover needs first, then plan for wants.") + "\n" + FALLBACK_TAG |
| if "earn" in t: |
| return ("You earn money by doing work or providing value. Small jobs add up. " |
| "Earnings give you choices to spend, save, or share, and teach the value of effort.") + "\n" + FALLBACK_TAG |
| if "sav" in t: |
| return ("Saving means putting aside some money now for future needs or goals. " |
| "Start small and be consistent. A jar, a partner plan, or a bank account all help.") + "\n" + FALLBACK_TAG |
| if "spend" in t or "wisely" in t: |
| return ("Spending wisely means comparing prices, making a simple budget, and avoiding impulse buys. " |
| "Aim for best value so your goals stay on track.") + "\n" + FALLBACK_TAG |
| if "summary" in t or "journal" in t or "plan" in t: |
| return ("Quick recap: cover needs first, set a small saving goal, and make one spending rule for the week. " |
| "Write one action you will try before the next lesson.") + "\n" + FALLBACK_TAG |
| if "quiz" in t: |
| return ("Take a short quiz to check your understanding of this lesson.") + "\n" + FALLBACK_TAG |
| |
| return (f"This unit will be populated from lesson_{module_id}/topic_{topic_ordinal}.txt. " |
| "For now, review the key idea and write one example from daily life.") + "\n" + FALLBACK_TAG |
|
|
| |
| |
| |
| def _render_catalog(): |
| st.header("Financial Education") |
| st.caption("Build your financial knowledge with structured paths for every skill level.") |
| |
| |
| st.markdown(""" |
| <style> |
| .card { |
| background: white; |
| padding: 1.5rem; |
| border-radius: 12px; |
| border: 1px solid #e1e5e9; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
| margin-bottom: 1rem; |
| height: 100%; |
| } |
| .card-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-start; |
| margin-bottom: 1rem; |
| } |
| .level-badge { |
| background: #f8f9fa; |
| color: #6c757d; |
| padding: 0.25rem 0.75rem; |
| border-radius: 20px; |
| font-size: 0.875rem; |
| font-weight: 500; |
| } |
| .card-title { |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: #212529; |
| margin: 0.5rem 0; |
| } |
| .card-description { |
| color: #6c757d; |
| font-size: 0.95rem; |
| line-height: 1.4; |
| margin-bottom: 1rem; |
| } |
| .card-meta { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin: 1rem 0; |
| color: #6c757d; |
| font-size: 0.875rem; |
| } |
| .meta-item { |
| display: flex; |
| align-items: center; |
| gap: 0.25rem; |
| } |
| .learn-section { |
| margin: 1rem 0; |
| } |
| .learn-title { |
| font-size: 0.75rem; |
| font-weight: 600; |
| color: #6c757d; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| margin-bottom: 0.5rem; |
| } |
| .learn-item { |
| color: #28a745; |
| font-size: 0.9rem; |
| margin-bottom: 0.25rem; |
| } |
| .primary-button, .secondary-button { |
| width: 100% !important; |
| border: none !important; |
| border-radius: 6px !important; |
| padding: 0.75rem 1.5rem !important; |
| font-weight: 500 !important; |
| margin-top: 1rem !important; |
| } |
| .primary-button { |
| background: #28a745 !important; |
| color: white !important; |
| } |
| .secondary-button { |
| background: #f8f9fa !important; |
| color: #495057 !important; |
| border: 1px solid #dee2e6 !important; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| level = st.session_state.get("level", "beginner") |
| |
| cols = st.columns(2, gap="large") |
| |
| for i, mod in enumerate(MODULES_META[level]): |
| with cols[i % 2]: |
| with st.container(): |
| st.markdown(f""" |
| <div class="card"> |
| <div class="card-header"> |
| <span class="level-badge">{mod['difficulty']}</span> |
| </div> |
| <h3 class="card-title">{mod['title']}</h3> |
| <p class="card-description">{mod['description']}</p> |
| <div class="card-meta"> |
| <div class="meta-item">🕒 {mod['duration']}</div> |
| <div class="meta-item">👥 {2500 - i * 350}</div> |
| </div> |
| <div class="learn-section"> |
| <div class="learn-title">You'll Learn</div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| with st.expander("📋 All Topics"): |
| for topic in mod['topics']: |
| st.write(f"• {topic}") |
|
|
| button_class = mod.get('button_style', 'primary') |
| button_text = "Start Learning" if button_class == 'primary' else "Review Module" |
| |
| if st.button( |
| f"{button_text} →", |
| key=f"start_{level}_{mod['id']}", |
| help=f"Begin the {mod['title']} module" |
| ): |
| st.session_state.module_id = mod["id"] |
| st.session_state.topic_idx = 0 |
| st.session_state.mode = "lesson" |
| st.rerun() |
| |
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
|
|
| def _get_topics(level: str, module_id: int) -> List[Tuple[str, str]]: |
| """ |
| Build the six-topic plan from metadata titles, try backend for each, |
| and if content is missing, provide a short fallback paragraph. |
| """ |
| cache_key = (level, module_id) |
| if cache_key in st.session_state.topics_cache: |
| return st.session_state.topics_cache[cache_key] |
|
|
| plan = _topic_plan(level, module_id) |
| out: List[Tuple[str, str]] = [] |
|
|
| for idx in range(len(plan)): |
| title, content = _fetch_topic_from_backend(level, module_id, idx) |
| if not content: |
| content = _fallback_text(title, module_id, idx + 1) |
| out.append((title, content)) |
|
|
| st.session_state.topics_cache[cache_key] = out |
| return out |
|
|
|
|
| def _render_lesson(): |
|
|
| """Render the lesson interface""" |
| level = st.session_state.get("level", "beginner") |
| module_id = st.session_state.get("module_id") |
|
|
| if module_id is None: |
| st.session_state.mode = "catalog" |
| st.rerun() |
|
|
| mod = next(m for m in MODULES_META[level] if m["id"] == module_id) |
|
|
| |
| |
| |
| st.markdown(""" |
| <style> |
| .lesson-header { |
| font-size: 2rem; |
| font-weight: 600; |
| color: #1f2937; |
| margin-bottom: 2rem; |
| } |
| .lesson-card { |
| background: white; |
| border: 1px solid #e5e7eb; |
| border-radius: 12px; |
| padding: 2rem; |
| margin-bottom: 1.5rem; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| min-height: 400px; |
| } |
| .unit-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1.5rem; |
| padding-bottom: 1rem; |
| border-bottom: 1px solid #f3f4f6; |
| } |
| .unit-info { |
| font-size: 0.875rem; |
| color: #6b7280; |
| font-weight: 500; |
| } |
| .read-time { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| font-size: 0.875rem; |
| color: #6b7280; |
| } |
| .lesson-content { |
| font-size: 1rem; |
| line-height: 1.6; |
| color: #374151; |
| margin-bottom: 2rem; |
| } |
| |
| |
| |
| .sidebar-card { |
| background: white; |
| border: 1px solid #e5e7eb; |
| border-radius: 12px; |
| padding: 1.5rem; |
| margin-bottom: 1rem; |
| } |
| .sidebar-card h4 { |
| font-size: 1.1rem; |
| font-weight: 600; |
| color: #1f2937; |
| margin-bottom: 1rem; |
| } |
| .unit-item { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| padding: 0.5rem 0; |
| font-size: 0.9rem; |
| } |
| .unit-active { |
| background: #10b981; |
| color: white; |
| padding: 0.25rem 0.75rem; |
| border-radius: 20px; |
| font-weight: 500; |
| } |
| .unit-inactive { |
| color: #6b7280; |
| } |
| .nav-buttons { |
| display: flex; |
| justify-content: space-between; |
| margin-top: 2rem; |
| gap: 1rem; |
| } |
| .btn-secondary { |
| background: #f3f4f6; |
| color: #374151; |
| border: 1px solid #d1d5db; |
| padding: 0.75rem 1.5rem; |
| border-radius: 8px; |
| font-weight: 500; |
| text-decoration: none; |
| } |
| .btn-primary { |
| background: #10b981; |
| color: white; |
| border: none; |
| padding: 0.75rem 1.5rem; |
| border-radius: 8px; |
| font-weight: 500; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown(f'<div class="lesson-header">{mod["title"]}</div>', unsafe_allow_html=True) |
|
|
| topics = _get_topics(level, module_id) |
| topic_idx = st.session_state.get("topic_idx", 0) |
|
|
| if not topics: |
| st.error("No lesson content available for this module.") |
| return |
|
|
| col_main, col_sidebar = st.columns([2, 1]) |
| |
| with col_main: |
| t_title, t_text = topics[topic_idx] |
|
|
| st.markdown(f""" |
| <div class="lesson-card"> |
| <div class="unit-header"> |
| <span class="unit-info">Unit {topic_idx + 1} of {len(topics)}</span> |
| <div class="read-time"> |
| <span>🕐</span> |
| <span>5-8 min read</span> |
| </div> |
| </div> |
| <div class="lesson-content"> |
| {t_text.strip() if t_text else "<p>Content coming soon.</p>"} |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| |
| st.markdown('<div class="nav-buttons">', unsafe_allow_html=True) |
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col1: |
| if st.button("← Previous", disabled=topic_idx == 0, key="prev_btn"): |
| st.session_state.topic_idx -= 1 |
| st.rerun() |
|
|
| with col3: |
| is_last = topic_idx >= len(topics) - 1 |
| if is_last: |
| if st.button("Complete Module →", key="complete_btn"): |
| st.session_state.show_quiz_prompt = True |
| _quiz_prompt_dialog() |
| st.stop() |
|
|
| else: |
| if st.button("Next Unit →", key="next_btn"): |
| st.session_state.topic_idx += 1 |
| st.rerun() |
| st.markdown('</div>', unsafe_allow_html=True) |
|
|
| |
| if st.session_state.get("show_quiz_prompt"): |
| _quiz_prompt_dialog() |
|
|
| |
|
|
| |
| |
| |
| with col_sidebar: |
| |
| st.markdown(""" |
| <div class="sidebar-card"> |
| <h4>Module Progress</h4> |
| """, unsafe_allow_html=True) |
| progress = (topic_idx + 1) / max(1, len(topics)) |
| st.progress(progress) |
| st.markdown(f"<p style='color: #6b7280; font-size: 0.875rem; margin-top: 0.5rem;'>Unit {topic_idx + 1} of {len(topics)}</p>", unsafe_allow_html=True) |
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| |
| st.markdown(""" |
| <div class="sidebar-card"> |
| <h4>Module Units</h4> |
| """, unsafe_allow_html=True) |
|
|
| for i, (tt, _) in enumerate(topics): |
| if i == topic_idx: |
| st.markdown(f""" |
| <div class="unit-item"> |
| <span style="background: #10b981; color: white; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem;">●</span> |
| <span class="unit-active">{tt}</span> |
| </div> |
| """, unsafe_allow_html=True) |
| elif i < topic_idx: |
| st.markdown(f""" |
| <div class="unit-item"> |
| <span style="color: #10b981; font-size: 1.1rem;">✓</span> |
| <span style="color: #374151;">{tt}</span> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| st.markdown(f""" |
| <div class="unit-item"> |
| <span style="background: #e5e7eb; color: #9ca3af; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem;">○</span> |
| <span class="unit-inactive">{tt}</span> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| |
| if st.button("← Back to Modules", key="back_modules"): |
| st.session_state.mode = "catalog" |
| st.session_state.module_id = None |
| st.session_state.topic_idx = 0 |
| st.rerun() |
|
|
|
|
|
|
| def _render_results(): |
| result = st.session_state.quiz_result or {} |
| score = result.get("score", {}) |
| correct = score.get("correct", 0) |
| total = score.get("total", 0) |
|
|
| st.success(f"Quiz Complete! You scored {correct} / {total}.") |
|
|
| wrong = result.get("wrong", []) |
| if wrong: |
| with st.expander("Review your answers"): |
| for w in wrong: |
| st.markdown(f"**{w.get('question','')}**") |
| st.write(f"Your answer: {w.get('your_answer','')}") |
| st.write(f"Correct answer: {w.get('correct_answer','')}") |
| st.divider() |
|
|
| fb = st.session_state.chatbot_feedback |
| if fb: |
| st.markdown("#### Tutor Explanation") |
| st.write(fb) |
|
|
| level = st.session_state.level |
| module_id = st.session_state.module_id |
| planned = next((m.get("topics", []) for m in MODULES_META[level] if m["id"] == module_id), []) |
|
|
| c1, c2, c3 = st.columns([1, 1, 1]) |
| with c1: |
| if st.button("Back to Modules"): |
| st.session_state.mode = "catalog" |
| st.session_state.module_id = None |
| st.rerun() |
| with c2: |
| if st.button("Ask the Chatbot →"): |
| st.session_state.current_page = "Chatbot" |
| st.session_state.chatbot_prefill = fb |
| st.rerun() |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None): |
| data = _fetch_assigned_lesson(lesson_id) |
| if not data or not data.get("lesson"): |
| st.error("Lesson not found or not available.") |
| if st.button("⬅ Back to Classes"): |
| st.session_state.lesson_route = None |
| st.session_state.current_page = "Teacher Link" |
| st.rerun() |
| return |
|
|
| L = data["lesson"] |
| sections = sorted(data.get("sections", []), key=lambda s: int(s.get("position", 0))) |
|
|
| st.markdown(f"### {L.get('title', 'Untitled')}") |
| if L.get("description"): |
| st.caption(L["description"]) |
|
|
| key = f"_dbsec_idx_{lesson_id}" |
| if key not in st.session_state: |
| st.session_state[key] = 0 |
|
|
| idx = st.session_state[key] |
| total = max(1, len(sections)) |
| st.progress((idx + 1) / total, text=f"Unit {idx + 1} of {total}") |
|
|
| if not sections: |
| st.info("No sections have been added to this lesson yet.") |
| if st.button("⬅ Back to Classes"): |
| st.session_state.lesson_route = None |
| st.session_state.current_page = "Teacher Link" |
| st.rerun() |
| return |
|
|
| sec = sections[idx] |
| sec_title = sec.get("title") or f"Section {idx + 1}" |
| content = (sec.get("content") or sec.get("body") or "").strip() |
|
|
| st.subheader(sec_title) |
| if content: |
| st.markdown(content, unsafe_allow_html=True) |
| else: |
| st.info("Content coming soon.") |
|
|
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col1: |
| if st.button("← Previous", disabled=idx == 0): |
| st.session_state[key] = max(0, idx - 1) |
| st.rerun() |
| with col2: |
| if st.button("Back to Classes"): |
| st.session_state.lesson_route = None |
| st.session_state.current_page = "Teacher Link" |
| st.rerun() |
| with col3: |
| is_last = idx >= len(sections) - 1 |
| if is_last: |
| if st.button("Finish"): |
| st.success("Lesson complete.") |
| st.session_state.lesson_route = None |
| st.session_state.current_page = "Teacher Link" |
| st.rerun() |
| else: |
| if st.button("Next →"): |
| st.session_state[key] = idx + 1 |
| st.rerun() |
|
|
| def render(): |
| load_css(os.path.join("assets", "styles.css")) |
| _ensure_state() |
| |
| route = st.session_state.get("lesson_route") |
| if route and route.get("source") == "teacher": |
| _render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id")) |
| return |
|
|
| st.caption("Learning Path · " + st.session_state.level.capitalize()) |
|
|
| mode = st.session_state.mode |
| if mode == "catalog": |
| _render_catalog() |
| elif mode == "lesson": |
| _render_lesson() |
| elif mode == "quiz": |
| quiz_page.show_page() |
| elif mode == "results": |
| _render_results() |
| else: |
| st.session_state.mode = "catalog" |
| _render_catalog() |
|
|
|
|
| |
| show_page = render |
|
|
| if __name__ == "__main__": |
| st.set_page_config(page_title="Lesson", page_icon="📘", layout="centered") |
| render() |
|
|