| |
| import os |
| import io |
| import csv |
| import datetime |
| import streamlit as st |
| import plotly.graph_objects as go |
|
|
| from utils import db as dbapi |
| import utils.api as api |
|
|
| |
| USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" |
|
|
| def load_css(file_name): |
| 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.") |
|
|
| def tile(icon, label, value): |
| return f""" |
| <div class="metric-card"> |
| <div class="metric-icon">{icon}</div> |
| <div class="metric-value">{value}</div> |
| <div class="metric-label">{label}</div> |
| </div> |
| """ |
|
|
| def _level_from_xp(xp: int) -> int: |
| """ |
| Prefer backend/db helper if available, else simple fallback (every 500 XP = +1 level). |
| """ |
| try: |
| if USE_LOCAL_DB and hasattr(dbapi, "level_from_xp"): |
| return int(dbapi.level_from_xp(xp)) |
| if hasattr(api, "level_from_xp"): |
| return int(api.level_from_xp(int(xp))) |
| except Exception: |
| pass |
| xp = int(xp or 0) |
| return 1 + (xp // 500) |
|
|
| def _safe_get_tiles(teacher_id: int) -> dict: |
| if USE_LOCAL_DB and hasattr(dbapi, "teacher_tiles"): |
| return dbapi.teacher_tiles(teacher_id) |
| try: |
| return api.teacher_tiles(teacher_id) |
| except Exception: |
| return { |
| "total_students": 0, "class_avg": 0.0, |
| "lessons_created": 0, "active_students": 0 |
| } |
|
|
| def _safe_list_classes(teacher_id: int) -> list: |
| if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"): |
| return dbapi.list_classes_by_teacher(teacher_id) |
| try: |
| return api.list_classes_by_teacher(teacher_id) |
| except Exception: |
| return [] |
|
|
| def _safe_create_class(teacher_id: int, name: str) -> dict: |
| if USE_LOCAL_DB and hasattr(dbapi, "create_class"): |
| return dbapi.create_class(teacher_id, name) |
| return api.create_class(teacher_id, name) |
|
|
| def _safe_class_student_metrics(class_id: int) -> list: |
| if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"): |
| return dbapi.class_student_metrics(class_id) |
| try: |
| return api.class_student_metrics(class_id) |
| except Exception: |
| return [] |
|
|
| def _safe_weekly_activity(class_id: int) -> list: |
| if USE_LOCAL_DB and hasattr(dbapi, "class_weekly_activity"): |
| return dbapi.class_weekly_activity(class_id) |
| try: |
| return api.class_weekly_activity(class_id) |
| except Exception: |
| return [] |
|
|
| def _safe_progress_overview(class_id: int) -> dict: |
| if USE_LOCAL_DB and hasattr(dbapi, "class_progress_overview"): |
| return dbapi.class_progress_overview(class_id) |
| try: |
| return api.class_progress_overview(class_id) |
| except Exception: |
| return { |
| "overall_progress": 0.0, "quiz_performance": 0.0, |
| "lessons_completed": 0, "class_xp": 0 |
| } |
|
|
| def _safe_recent_activity(class_id: int, limit=6, days=30) -> list: |
| if USE_LOCAL_DB and hasattr(dbapi, "class_recent_activity"): |
| return dbapi.class_recent_activity(class_id, limit=limit, days=days) |
| try: |
| return api.class_recent_activity(class_id, limit=limit, days=days) |
| except Exception: |
| return [] |
|
|
| def _safe_list_students(class_id: int) -> list: |
| if USE_LOCAL_DB and hasattr(dbapi, "list_students_in_class"): |
| return dbapi.list_students_in_class(class_id) |
| try: |
| return api.list_students_in_class(class_id) |
| except Exception: |
| return [] |
|
|
| def show_teacher_dashboard(): |
| css_path = os.path.join("assets", "styles.css") |
| load_css(css_path) |
|
|
| user = st.session_state.user |
| teacher_id = user["user_id"] |
| name = user["name"] |
|
|
| |
| colH1, colH2 = st.columns([5, 2]) |
| with colH1: |
| st.markdown(f""" |
| <div class="header-container"> |
| <div class="header-content"> |
| <div class="header-left"> |
| <div class="header-title">Welcome back, Teacher {name}!</div> |
| <div class="header-subtitle">Managing your classrooms</div> |
| </div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| with colH2: |
| with st.popover("โ Create Classroom"): |
| new_class_name = st.text_input("Classroom Name", key="new_class_name") |
| if st.button("Create Classroom", key="create_classroom_btn"): |
| if new_class_name.strip(): |
| try: |
| out = _safe_create_class(teacher_id, new_class_name.strip()) |
| code = out.get("code") or out.get("class_code") or "โ" |
| st.success(f"Classroom created. Code: **{code}**") |
| except Exception as e: |
| st.error(f"Create failed: {e}") |
|
|
| |
| tiles = _safe_get_tiles(teacher_id) |
| c1,c2,c3,c4 = st.columns(4) |
| c1.markdown(tile("๐ฅ","Total Students", tiles.get("total_students", 0)), unsafe_allow_html=True) |
| c2.markdown(tile("๐","Class Average", f"{int((tiles.get('class_avg') or 0)*100)}%"), unsafe_allow_html=True) |
| c3.markdown(tile("๐","Lessons Created", tiles.get("lessons_created", 0)), unsafe_allow_html=True) |
| c4.markdown(tile("๐","Active Students", tiles.get("active_students", 0)), unsafe_allow_html=True) |
|
|
| |
| classes = _safe_list_classes(teacher_id) |
| if not classes: |
| st.info("No classes yet. Create one above, then share the code with students.") |
| return |
|
|
| idx = st.selectbox( |
| "Choose a class", |
| list(range(len(classes))), |
| index=0, |
| format_func=lambda i: f"{classes[i].get('name','Class')} (Code: {classes[i].get('code','')})" |
| ) |
| selected = classes[idx] |
| class_id = selected.get("class_id") or selected.get("id") |
| class_code = selected.get("code","") |
|
|
| |
| cTop1, cTop2, cTop3 = st.columns([2,1,1]) |
| with cTop1: |
| st.button(f"Class Code: {class_code}", disabled=True) |
| with cTop2: |
| if st.button("๐ Copy Code"): |
| st.toast("Code copied. Paste it anywhere your heart desires.") |
| with cTop3: |
| rows = _safe_class_student_metrics(class_id) |
| if rows: |
| headers = [] |
| for r in rows: |
| for k in r.keys(): |
| if k not in headers: |
| headers.append(k) |
| buf = io.StringIO() |
| writer = csv.DictWriter(buf, fieldnames=headers) |
| writer.writeheader() |
| for r in rows: |
| writer.writerow(r) |
| st.download_button( |
| "๐ค Export Class Report", |
| data=buf.getvalue(), |
| file_name=f"class_{class_id}_report.csv", |
| mime="text/csv" |
| ) |
| else: |
| st.button("๐ค Export Class Report", disabled=True) |
|
|
| |
| left, right = st.columns([3,2]) |
|
|
| with left: |
| st.subheader("Weekly Activity") |
| st.caption("Student engagement throughout the week") |
| activity = _safe_weekly_activity(class_id) |
| if activity: |
| days, lessons, quizzes, games = [], [], [], [] |
| for row in activity: |
| date_str = row.get("date") |
| try: |
| |
| day = datetime.datetime.fromisoformat(str(date_str)).strftime("%a") |
| except Exception: |
| day = str(date_str) |
| days.append(day) |
| lessons.append(row.get("lessons",0)) |
| quizzes.append(row.get("quizzes",0)) |
| games.append(row.get("games",0)) |
| fig = go.Figure(data=[ |
| go.Bar(name="Lessons", x=days, y=lessons), |
| go.Bar(name="Quizzes", x=days, y=quizzes), |
| go.Bar(name="Games", x=days, y=games), |
| ]) |
| fig.update_layout(barmode="group", xaxis_title="Day", yaxis_title="Count") |
| st.plotly_chart(fig, use_container_width=True) |
| else: |
| st.info("No activity in the last 7 days.") |
|
|
| with right: |
| st.subheader("Class Progress Overview") |
| st.caption("How your students are performing") |
|
|
| prog = _safe_progress_overview(class_id) |
| overall_pct = int(round((prog.get("overall_progress") or 0) * 100)) |
| quiz_pct = int(round((prog.get("quiz_performance") or 0) * 100)) |
|
|
| st.text("Overall Progress") |
| st.progress(min(1.0, overall_pct/100.0)) |
| st.caption(f"{overall_pct}%") |
|
|
| st.text("Quiz Performance") |
| st.progress(min(1.0, quiz_pct/100.0)) |
| st.caption(f"{quiz_pct}%") |
|
|
| k1, k2 = st.columns(2) |
| k1.metric("๐ Lessons Completed", prog.get("lessons_completed", 0)) |
| k2.metric("๐ช Total Class XP", prog.get("class_xp", 0)) |
|
|
| |
| b1, b2 = st.columns([3,2]) |
|
|
| with b1: |
| st.subheader("Recent Student Activity") |
| st.caption("Latest activity from your students") |
| feed = _safe_recent_activity(class_id, limit=6, days=30) |
| if not feed: |
| st.caption("Nothing yet. Assign something, chief.") |
| else: |
| for r in feed: |
| kind = str(r.get("kind","")).lower() |
| icon = "๐" if kind == "lesson" else "๐" if kind == "quiz" else "๐ฎ" |
| lvl = r.get("level") or _level_from_xp(r.get("total_xp", 0)) |
| tail = f" ยท {r['extra']}" if r.get("extra") else "" |
| st.write(f"{icon} **{r.get('student_name','(unknown)')}** โ {r.get('item_title','(untitled)')}{tail} \n" |
| f"*Level {lvl}*") |
|
|
| with b2: |
| st.subheader("Quick Actions") |
| st.caption("Manage your classroom") |
| if st.button("๐ Create New Lesson", use_container_width=True): |
| st.session_state.current_page = "Content Management" |
| st.rerun() |
| if st.button("๐ Create New Quiz", use_container_width=True): |
| st.session_state.current_page = "Content Management" |
| st.rerun() |
| if st.button("๐๏ธ Schedule Assignment", use_container_width=True): |
| st.session_state.current_page = "Class management" |
| st.rerun() |
| if st.button("๐ Generate Reports", use_container_width=True): |
| st.session_state.current_page = "Students List" |
| st.rerun() |
|
|
| |
| for c in classes: |
| with st.expander(f"{c.get('name','Class')} ยท Code **{c.get('code','')}**"): |
| st.write(f"Students: {c.get('total_students', 0)}") |
| avg = c.get("class_avg", 0.0) |
| st.write(f"Average score: {round(float(avg)*100) if avg is not None else 0}%") |
| roster = _safe_list_students(c.get("class_id") or c.get("id")) |
| if roster: |
| for s in roster: |
| lvl_slug = (s.get("level_slug") or s.get("level") or "beginner") |
| st.write(f"- {s.get('name','(unknown)')} ยท {s.get('email','โ')} ยท Level {str(lvl_slug).capitalize()}") |
| else: |
| st.caption("No students yet. Share the code.") |
|
|