| |
| import os |
| import streamlit as st |
|
|
| from utils import db as dbapi |
| import utils.api as api |
|
|
| |
| USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" |
|
|
|
|
| def _metric_card(label: str, value: str, caption: str = ""): |
| st.markdown( |
| f""" |
| <div class="metric-card"> |
| <div class="metric-value">{value}</div> |
| <div class="metric-label">{label}</div> |
| <div class="metric-caption">{caption}</div> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def _prefer_db(db_name: str, api_func, default, *args, **kwargs): |
| """ |
| Try local DB function if enabled & present; else call backend API; else return default. |
| """ |
| if USE_LOCAL_DB and hasattr(dbapi, db_name): |
| try: |
| return getattr(dbapi, db_name)(*args, **kwargs) |
| except Exception as e: |
| st.warning(f"DB call {db_name} failed; falling back to backend. ({e})") |
| try: |
| return api_func(*args, **kwargs) |
| except Exception as e: |
| st.error(f"Backend call failed: {e}") |
| return default |
|
|
|
|
| def show_page(): |
| user = st.session_state.user |
| teacher_id = user["user_id"] |
|
|
| st.title("π Classroom Management") |
| st.caption("Manage all your classrooms and students") |
|
|
| |
| with st.expander("β Create Classroom", expanded=False): |
| new_name = st.text_input("Classroom Name", key="new_classroom_name") |
| if st.button("Create Classroom"): |
| name = new_name.strip() |
| if not name: |
| st.error("Enter a real name, not whitespace.") |
| else: |
| out = _prefer_db( |
| "create_class", |
| lambda tid, n: api.create_class(tid, n), |
| None, |
| teacher_id, |
| name, |
| ) |
| if out: |
| st.session_state.selected_class_id = out.get("class_id") or out.get("id") |
| st.success(f'Classroom "{name}" created with code: {out.get("code","β")}') |
| st.rerun() |
| else: |
| st.error("Could not create classroom (no response).") |
|
|
| |
| classes = _prefer_db( |
| "list_classes_by_teacher", |
| lambda tid: api.list_classes_by_teacher(tid), |
| [], |
| teacher_id, |
| ) |
|
|
| if not classes: |
| st.info("No classrooms yet. Create one above, then share the code.") |
| return |
|
|
| |
| st.subheader("Your Classrooms") |
| options = {f"{c.get('name','(unnamed)')} (Code: {c.get('code','')})": c for c in classes} |
| selected_label = st.selectbox("Select a classroom", list(options.keys())) |
| selected = options[selected_label] |
| class_id = selected.get("class_id") or selected.get("id") |
|
|
| st.markdown("---") |
| st.header(selected.get("name", "Classroom")) |
|
|
| |
| st.subheader("Class Code") |
| c1, c2, c3 = st.columns([3, 1, 1]) |
| with c1: |
| st.markdown(f"**`{selected.get('code', 'UNKNOWN')}`**") |
| with c2: |
| if st.button("π Copy Code"): |
| st.toast("Code is shown above. Copy it.") |
| with c3: |
| st.button("ποΈ Delete Class", disabled=True, help="Soft-delete coming later") |
|
|
| |
| tab_students, tab_content, tab_analytics = st.tabs(["π₯ Students", "π Content", "π Analytics"]) |
|
|
| |
| with tab_students: |
| q = st.text_input("Search students by name or email", "") |
| roster = _prefer_db( |
| "list_students_in_class", |
| lambda cid: api.list_students_in_class(cid), |
| [], |
| class_id, |
| ) |
|
|
| |
| if q.strip(): |
| ql = q.lower() |
| roster = [r for r in roster if ql in (r.get("name","").lower()) or ql in (r.get("email","").lower())] |
|
|
| st.caption(f"{len(roster)} Students Found") |
|
|
| if not roster: |
| st.info("No students in this class yet.") |
| else: |
| for s in roster: |
| st.subheader(f"π€ {s.get('name','(unknown)')}") |
| st.caption(s.get("email","β")) |
| joined = s.get("joined_at") or s.get("created_at") |
| st.caption(f"π
Joined: {str(joined)[:10] if joined else 'β'}") |
| st.progress(0.0) |
| cols = st.columns(3) |
| level_slug = (s.get("level_slug") or s.get("level") or "beginner") |
| try: |
| level_label = level_slug.capitalize() if isinstance(level_slug, str) else str(level_slug) |
| except Exception: |
| level_label = "β" |
| cols[0].metric("β Level", level_label) |
| cols[1].metric("π Avg Score", "β") |
| cols[2].metric("π₯ Streak", "β") |
| st.markdown("---") |
|
|
| |
| with tab_content: |
| counts = _prefer_db( |
| "class_content_counts", |
| lambda cid: api.class_content_counts(cid), |
| {"lessons": 0, "quizzes": 0}, |
| class_id, |
| ) |
| left, right = st.columns(2) |
| with left: |
| _metric_card("π Custom Lessons", str(counts.get("lessons", 0)), "Lessons created for this classroom") |
| with right: |
| _metric_card("π Custom Quizzes", str(counts.get("quizzes", 0)), "Quizzes created for this classroom") |
|
|
| assigs = _prefer_db( |
| "list_class_assignments", |
| lambda cid: api.list_class_assignments(cid), |
| [], |
| class_id, |
| ) |
| if assigs: |
| st.markdown("#### Assigned items") |
| for a in assigs: |
| has_quiz = " + Quiz" if a.get("quiz_id") else "" |
| st.markdown(f"- **{a.get('title','Untitled')}** Β· {a.get('subject','β')} Β· {a.get('level','β')}{has_quiz}") |
|
|
| |
| with tab_analytics: |
| stats = _prefer_db( |
| "class_analytics", |
| lambda cid: api.class_analytics(cid), |
| {"class_avg": 0.0, "total_xp": 0, "lessons_completed": 0}, |
| class_id, |
| ) |
|
|
| class_avg_pct = round(float(stats.get("class_avg", 0)) * 100) if stats.get("class_avg") is not None else 0 |
| total_xp = stats.get("total_xp", 0) |
| lessons_completed = stats.get("lessons_completed", 0) |
|
|
| g1, g2, g3 = st.columns(3) |
| with g1: |
| _metric_card("π Class Average", f"{class_avg_pct}%", "Average quiz performance") |
| with g2: |
| _metric_card("πͺ Total XP", f"{total_xp}", "Combined XP earned") |
| with g3: |
| _metric_card("π Lessons Completed", f"{lessons_completed}", "Total lessons completed") |
|
|