Download phase/Student_view/lesson.py from FinEDProj/FInFront: direct link, hf CLI and curl.
- Browser
- Download file 27.3 kB
-
https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/phase/Student_view/lesson.py
- Command line
-
hf download hf://spaces/FinEDProj/FInFront/phase/Student_view/lesson.py
-
curl -L -o lesson.py https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/phase/Student_view/lesson.py
27.3 kB
| 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 # unified backend client | |
| from phase.Student_view import quiz as quiz_page | |
| USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" | |
| FALLBACK_TAG = "<!--fallback-->" | |
| # --- Load external CSS (optional) --- | |
| 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.") | |
| # --------------------------------------------- | |
| # Page state helpers | |
| # --------------------------------------------- | |
| _SS_DEFAULTS = { | |
| "level": "beginner", # beginner | intermediate | advanced | |
| "module_id": None, # int (1-based) | |
| "topic_idx": 0, # 0-based within module | |
| "mode": "catalog", # catalog | lesson | quiz | results | |
| "topics_cache": {}, # {(level, module_id): [(title, text), ...]} | |
| "show_quiz_prompt": False, # <-- controls the post-module popup | |
| "selected_quiz": None, | |
| } | |
| # Works on Streamlit versions that support st.dialog | |
| if hasattr(st, "dialog"): | |
| 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: | |
| # Fallback: inline banner if st.dialog isn't available | |
| 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 {} | |
| # --------------------------------------------- | |
| # Content metadata (UI only) | |
| # --------------------------------------------- | |
| 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)) | |
| # Ensure at most 6 topics: first five + Summary if present | |
| 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)] | |
| # --------------------------------------------- | |
| # Backend integrations | |
| # --------------------------------------------- | |
| 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] # 1-based ordinal in original list | |
| 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 | |
| # Generic fallback | |
| 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 | |
| # --------------------------------------------- | |
| # UI building blocks | |
| # --------------------------------------------- | |
| def _render_catalog(): | |
| st.header("Financial Education") | |
| st.caption("Build your financial knowledge with structured paths for every skill level.") | |
| # Custom CSS for card styling | |
| 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) | |
| # ------------------------- | |
| # Styles | |
| # ------------------------- | |
| 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) | |
| # Navigation Buttons OUTSIDE lesson card | |
| 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() # open now | |
| st.stop() # prevent the rest of the page from rendering under it | |
| else: | |
| if st.button("Next Unit β", key="next_btn"): | |
| st.session_state.topic_idx += 1 | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ---- Post-module modal: offer to start Quiz 1 ---- | |
| if st.session_state.get("show_quiz_prompt"): | |
| _quiz_prompt_dialog() | |
| # ----------------------------- | |
| # Sidebar with Progress & Units | |
| # ----------------------------- | |
| with col_sidebar: | |
| # Progress Card | |
| 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) | |
| # Units Card | |
| 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) | |
| # Back Button | |
| 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() | |
| # with c3: | |
| # if quiz_index is not None and quiz_index + 1 < len(planned): | |
| # if st.button("Continue Lesson β"): | |
| # st.session_state.mode = "lesson" | |
| # st.session_state.topic_idx = quiz_index + 1 | |
| # st.rerun() | |
| # --------------------------------------------- | |
| # Public entry point(s) | |
| # --------------------------------------------- | |
| 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() | |
| # For pages that import and call `show_page()` | |
| show_page = render | |
| if __name__ == "__main__": | |
| st.set_page_config(page_title="Lesson", page_icon="π", layout="centered") | |
| render() | |