lanna_lalala;- commited on
Commit
5a8b3c5
·
1 Parent(s): 4605217
dashboards/teacher_db.py CHANGED
@@ -1,12 +1,16 @@
1
  # dashboards/teacher_db.py
2
- import streamlit as st
3
  import os
4
- import plotly.express as px
5
- import plotly.graph_objects as go
6
- import csv
7
  import io
 
8
  import datetime
 
 
 
9
  from utils import db as dbapi
 
 
 
 
10
 
11
  def load_css(file_name):
12
  try:
@@ -24,6 +28,87 @@ def tile(icon, label, value):
24
  </div>
25
  """
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def show_teacher_dashboard():
28
  css_path = os.path.join("assets", "styles.css")
29
  load_css(css_path)
 
1
  # dashboards/teacher_db.py
 
2
  import os
 
 
 
3
  import io
4
+ import csv
5
  import datetime
6
+ import streamlit as st
7
+ import plotly.graph_objects as go
8
+
9
  from utils import db as dbapi
10
+ import utils.api as api # backend Space client
11
+
12
+ # If DISABLE_DB=1 (default), don't call MySQL at all
13
+ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
14
 
15
  def load_css(file_name):
16
  try:
 
28
  </div>
29
  """
30
 
31
+ def _level_from_xp(xp: int) -> int:
32
+ """
33
+ Prefer backend/db helper if available, else simple fallback (every 500 XP = +1 level).
34
+ """
35
+ try:
36
+ if USE_LOCAL_DB and hasattr(dbapi, "level_from_xp"):
37
+ return int(dbapi.level_from_xp(xp))
38
+ if hasattr(api, "level_from_xp"): # if you add the endpoint
39
+ return int(api.level_from_xp(int(xp)))
40
+ except Exception:
41
+ pass
42
+ xp = int(xp or 0)
43
+ return 1 + (xp // 500)
44
+
45
+ def _safe_get_tiles(teacher_id: int) -> dict:
46
+ if USE_LOCAL_DB and hasattr(dbapi, "teacher_tiles"):
47
+ return dbapi.teacher_tiles(teacher_id)
48
+ try:
49
+ return api.teacher_tiles(teacher_id)
50
+ except Exception:
51
+ return {
52
+ "total_students": 0, "class_avg": 0.0,
53
+ "lessons_created": 0, "active_students": 0
54
+ }
55
+
56
+ def _safe_list_classes(teacher_id: int) -> list:
57
+ if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"):
58
+ return dbapi.list_classes_by_teacher(teacher_id)
59
+ try:
60
+ return api.list_classes_by_teacher(teacher_id)
61
+ except Exception:
62
+ return []
63
+
64
+ def _safe_create_class(teacher_id: int, name: str) -> dict:
65
+ if USE_LOCAL_DB and hasattr(dbapi, "create_class"):
66
+ return dbapi.create_class(teacher_id, name)
67
+ return api.create_class(teacher_id, name)
68
+
69
+ def _safe_class_student_metrics(class_id: int) -> list:
70
+ if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"):
71
+ return dbapi.class_student_metrics(class_id)
72
+ try:
73
+ return api.class_student_metrics(class_id)
74
+ except Exception:
75
+ return []
76
+
77
+ def _safe_weekly_activity(class_id: int) -> list:
78
+ if USE_LOCAL_DB and hasattr(dbapi, "class_weekly_activity"):
79
+ return dbapi.class_weekly_activity(class_id)
80
+ try:
81
+ return api.class_weekly_activity(class_id)
82
+ except Exception:
83
+ return []
84
+
85
+ def _safe_progress_overview(class_id: int) -> dict:
86
+ if USE_LOCAL_DB and hasattr(dbapi, "class_progress_overview"):
87
+ return dbapi.class_progress_overview(class_id)
88
+ try:
89
+ return api.class_progress_overview(class_id)
90
+ except Exception:
91
+ return {
92
+ "overall_progress": 0.0, "quiz_performance": 0.0,
93
+ "lessons_completed": 0, "class_xp": 0
94
+ }
95
+
96
+ def _safe_recent_activity(class_id: int, limit=6, days=30) -> list:
97
+ if USE_LOCAL_DB and hasattr(dbapi, "class_recent_activity"):
98
+ return dbapi.class_recent_activity(class_id, limit=limit, days=days)
99
+ try:
100
+ return api.class_recent_activity(class_id, limit=limit, days=days)
101
+ except Exception:
102
+ return []
103
+
104
+ def _safe_list_students(class_id: int) -> list:
105
+ if USE_LOCAL_DB and hasattr(dbapi, "list_students_in_class"):
106
+ return dbapi.list_students_in_class(class_id)
107
+ try:
108
+ return api.list_students_in_class(class_id)
109
+ except Exception:
110
+ return []
111
+
112
  def show_teacher_dashboard():
113
  css_path = os.path.join("assets", "styles.css")
114
  load_css(css_path)
phase/Student_view/teacherlink.py CHANGED
@@ -2,6 +2,9 @@
2
  import os
3
  import streamlit as st
4
  from utils import db as dbapi
 
 
 
5
 
6
  def load_css(file_name: str):
7
  try:
@@ -18,6 +21,57 @@ def _progress_0_1(v):
18
  # accept 0..1 or 0..100
19
  return max(0.0, min(1.0, f if f <= 1.0 else f / 100.0))
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def show_code():
22
  load_css(os.path.join("assets", "styles.css"))
23
 
@@ -33,7 +87,7 @@ def show_code():
33
  st.caption("Enter class code from your teacher")
34
 
35
  raw = st.text_input(
36
- label="Class Code",
37
  placeholder="e.g. FIN5A2024",
38
  key="class_code_input",
39
  label_visibility="collapsed"
@@ -65,16 +119,18 @@ def show_code():
65
  st.error("Enter a class code.")
66
  else:
67
  try:
68
- class_id = dbapi.join_class_by_code(student_id, code)
69
  st.success("🎉 Joined the class!")
70
  st.rerun()
71
  except ValueError as e:
72
  st.error(str(e))
 
 
73
 
74
  st.markdown("---")
75
  st.markdown("## Your Classes")
76
 
77
- classes = dbapi.list_classes_for_student(student_id)
78
  if not classes:
79
  st.info("You haven’t joined any classes yet. Ask your teacher for a class code.")
80
  return
@@ -82,36 +138,44 @@ def show_code():
82
  # one card per class
83
  for c in classes:
84
  class_id = c["class_id"]
85
- counts = dbapi.class_content_counts(class_id) # lessons/quizzes count
86
- prog = dbapi.student_class_progress(student_id, class_id)
87
 
88
- st.markdown(f"### {c['name']}")
89
- st.caption(f"Teacher: {c['teacher_name']} • Code: {c['code']} • Joined: {str(c['joined_at'])[:10]}")
 
 
 
 
90
 
91
- st.progress(_progress_0_1(prog["overall_progress"]))
 
92
  st.caption(
93
- f"{prog['lessons_completed']}/{prog['total_assigned_lessons']} lessons completed • "
94
- f"Avg quiz: {int(round(100 * (prog['avg_score'] or 0)))}%"
95
  )
96
 
97
  # top metrics
98
  m1, m2, m3, m4 = st.columns(4)
99
  m1.metric("Lessons", counts.get("lessons", 0))
100
  m2.metric("Quizzes", counts.get("quizzes", 0))
101
- m3.metric("Overall", f"{int(round(100*_progress_0_1(prog['overall_progress'])))}%")
102
- m4.metric("Avg Quiz", f"{int(round(100*(prog['avg_score'] or 0)))}%")
103
 
104
  # Leave class
105
- leave_col, _ = st.columns([1,3])
106
  with leave_col:
107
  if st.button("🚪 Leave Class", key=f"leave_{class_id}"):
108
- dbapi.leave_class(student_id, class_id)
109
- st.toast("Left class.", icon="👋")
110
- st.rerun()
 
 
 
111
 
112
  # Assignments for THIS class with THIS student's progress
113
  st.markdown("#### Teacher Lessons & Quizzes")
114
- rows = dbapi.student_assignments_for_class(student_id, class_id)
115
  if not rows:
116
  st.info("No assignments yet.")
117
  else:
@@ -119,23 +183,29 @@ def show_code():
119
 
120
  with lessons_tab:
121
  for r in rows:
122
- if r["lesson_id"] is None:
123
  continue
124
 
125
  status = r.get("status") or "not_started"
126
  pos = r.get("current_pos") or 0
127
- pct = 1.0 if status == "completed" else min(0.95, float(pos or 0) * 0.1)
128
-
129
- st.subheader(r["title"])
130
- st.caption(f"{r['subject']} • {r['level']} • Due: {str(r['due_at'])[:10] if r.get('due_at') else '—'}")
 
 
 
 
 
 
131
  st.progress(_progress_0_1(pct))
132
 
133
  c1, c2 = st.columns(2)
134
  with c1:
135
- # pass lesson & assignment to the Lessons page
136
- if st.button("▶️ Start Lesson", key=f"start_lesson_{r['assignment_id']}"):
137
- st.session_state.selected_lesson = r["lesson_id"]
138
- st.session_state.selected_assignment = r["assignment_id"]
139
  st.session_state.current_page = "Lessons"
140
  st.rerun()
141
  with c2:
@@ -144,21 +214,26 @@ def show_code():
144
  with quizzes_tab:
145
  any_quiz = False
146
  for r in rows:
147
- if not r.get("quiz_id"):
 
148
  continue
149
  any_quiz = True
150
 
151
- st.subheader(r["title"])
152
  score, total = r.get("score"), r.get("total")
153
  if score is not None and total:
154
- st.caption(f"Last score: {int(round(100*float(score)/float(total)))}%")
 
 
 
 
155
  else:
156
  st.caption("No submission yet")
157
 
158
- # pass quiz & assignment to the Quiz page
159
- if st.button("📝 Start Quiz", key=f"start_quiz_{class_id}_{r['quiz_id']}"):
160
- st.session_state.selected_quiz = r["quiz_id"] # numeric quiz_id from DB
161
- st.session_state.current_assignment = r["assignment_id"] # you’ll need this when submitting
162
  st.session_state.current_page = "Quiz"
163
  st.rerun()
164
 
 
2
  import os
3
  import streamlit as st
4
  from utils import db as dbapi
5
+ import utils.api as api # <-- backend Space client
6
+
7
+ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" # DB only when DISABLE_DB=0
8
 
9
  def load_css(file_name: str):
10
  try:
 
21
  # accept 0..1 or 0..100
22
  return max(0.0, min(1.0, f if f <= 1.0 else f / 100.0))
23
 
24
+ # --- Small wrappers to switch between DB and Backend ---
25
+
26
+ def _join_class_by_code(student_id: int, code: str):
27
+ if USE_LOCAL_DB and hasattr(dbapi, "join_class_by_code"):
28
+ return dbapi.join_class_by_code(student_id, code)
29
+ return api.join_class_by_code(student_id, code)
30
+
31
+ def _list_classes_for_student(student_id: int):
32
+ if USE_LOCAL_DB and hasattr(dbapi, "list_classes_for_student"):
33
+ return dbapi.list_classes_for_student(student_id)
34
+ try:
35
+ return api.list_classes_for_student(student_id)
36
+ except Exception:
37
+ return []
38
+
39
+ def _class_content_counts(class_id: int):
40
+ if USE_LOCAL_DB and hasattr(dbapi, "class_content_counts"):
41
+ return dbapi.class_content_counts(class_id)
42
+ try:
43
+ return api.class_content_counts(class_id)
44
+ except Exception:
45
+ return {"lessons": 0, "quizzes": 0}
46
+
47
+ def _student_class_progress(student_id: int, class_id: int):
48
+ if USE_LOCAL_DB and hasattr(dbapi, "student_class_progress"):
49
+ return dbapi.student_class_progress(student_id, class_id)
50
+ try:
51
+ return api.student_class_progress(student_id, class_id)
52
+ except Exception:
53
+ return {
54
+ "overall_progress": 0,
55
+ "lessons_completed": 0,
56
+ "total_assigned_lessons": 0,
57
+ "avg_score": 0,
58
+ }
59
+
60
+ def _leave_class(student_id: int, class_id: int):
61
+ if USE_LOCAL_DB and hasattr(dbapi, "leave_class"):
62
+ return dbapi.leave_class(student_id, class_id)
63
+ return api.leave_class(student_id, class_id)
64
+
65
+ def _student_assignments_for_class(student_id: int, class_id: int):
66
+ if USE_LOCAL_DB and hasattr(dbapi, "student_assignments_for_class"):
67
+ return dbapi.student_assignments_for_class(student_id, class_id)
68
+ try:
69
+ return api.student_assignments_for_class(student_id, class_id)
70
+ except Exception:
71
+ return []
72
+
73
+ # --- UI ---
74
+
75
  def show_code():
76
  load_css(os.path.join("assets", "styles.css"))
77
 
 
87
  st.caption("Enter class code from your teacher")
88
 
89
  raw = st.text_input(
90
+ label="Class Code",
91
  placeholder="e.g. FIN5A2024",
92
  key="class_code_input",
93
  label_visibility="collapsed"
 
119
  st.error("Enter a class code.")
120
  else:
121
  try:
122
+ _join_class_by_code(student_id, code)
123
  st.success("🎉 Joined the class!")
124
  st.rerun()
125
  except ValueError as e:
126
  st.error(str(e))
127
+ except Exception as e:
128
+ st.error(f"Could not join class: {e}")
129
 
130
  st.markdown("---")
131
  st.markdown("## Your Classes")
132
 
133
+ classes = _list_classes_for_student(student_id)
134
  if not classes:
135
  st.info("You haven’t joined any classes yet. Ask your teacher for a class code.")
136
  return
 
138
  # one card per class
139
  for c in classes:
140
  class_id = c["class_id"]
141
+ counts = _class_content_counts(class_id) # lessons/quizzes count
142
+ prog = _student_class_progress(student_id, class_id)
143
 
144
+ st.markdown(f"### {c.get('name', 'Untitled Class')}")
145
+ st.caption(
146
+ f"Teacher: {c.get('teacher_name','—')} • "
147
+ f"Code: {c.get('code','—')} • "
148
+ f"Joined: {str(c.get('joined_at',''))[:10] if c.get('joined_at') else '—'}"
149
+ )
150
 
151
+ st.progress(_progress_0_1(prog.get("overall_progress", 0)))
152
+ avg_pct = int(round(100 * _progress_0_1(prog.get("avg_score", 0))))
153
  st.caption(
154
+ f"{prog.get('lessons_completed', 0)}/{prog.get('total_assigned_lessons', 0)} lessons completed • "
155
+ f"Avg quiz: {avg_pct}%"
156
  )
157
 
158
  # top metrics
159
  m1, m2, m3, m4 = st.columns(4)
160
  m1.metric("Lessons", counts.get("lessons", 0))
161
  m2.metric("Quizzes", counts.get("quizzes", 0))
162
+ m3.metric("Overall", f"{int(round(100 * _progress_0_1(prog.get('overall_progress', 0))))}%")
163
+ m4.metric("Avg Quiz", f"{avg_pct}%")
164
 
165
  # Leave class
166
+ leave_col, _ = st.columns([1, 3])
167
  with leave_col:
168
  if st.button("🚪 Leave Class", key=f"leave_{class_id}"):
169
+ try:
170
+ _leave_class(student_id, class_id)
171
+ st.toast("Left class.", icon="👋")
172
+ st.rerun()
173
+ except Exception as e:
174
+ st.error(f"Could not leave class: {e}")
175
 
176
  # Assignments for THIS class with THIS student's progress
177
  st.markdown("#### Teacher Lessons & Quizzes")
178
+ rows = _student_assignments_for_class(student_id, class_id)
179
  if not rows:
180
  st.info("No assignments yet.")
181
  else:
 
183
 
184
  with lessons_tab:
185
  for r in rows:
186
+ if r.get("lesson_id") is None:
187
  continue
188
 
189
  status = r.get("status") or "not_started"
190
  pos = r.get("current_pos") or 0
191
+ # if backend returns explicit progress % or 0..1, keep it sane:
192
+ pct = r.get("progress")
193
+ if pct is None:
194
+ # fallback: estimate from position
195
+ pct = 1.0 if status == "completed" else min(0.95, float(pos or 0) * 0.1)
196
+
197
+ st.subheader(r.get("title", "Untitled"))
198
+ due = r.get("due_at")
199
+ due_txt = f"Due: {str(due)[:10]}" if due else "—"
200
+ st.caption(f"{r.get('subject','General')} • {r.get('level','Beginner')} • {due_txt}")
201
  st.progress(_progress_0_1(pct))
202
 
203
  c1, c2 = st.columns(2)
204
  with c1:
205
+ # pass lesson & assignment to the Lessons page
206
+ if st.button("▶️ Start Lesson", key=f"start_lesson_{r.get('assignment_id')}"):
207
+ st.session_state.selected_lesson = r.get("lesson_id")
208
+ st.session_state.selected_assignment = r.get("assignment_id")
209
  st.session_state.current_page = "Lessons"
210
  st.rerun()
211
  with c2:
 
214
  with quizzes_tab:
215
  any_quiz = False
216
  for r in rows:
217
+ quiz_id = r.get("quiz_id")
218
+ if not quiz_id:
219
  continue
220
  any_quiz = True
221
 
222
+ st.subheader(r.get("title", "Untitled"))
223
  score, total = r.get("score"), r.get("total")
224
  if score is not None and total:
225
+ try:
226
+ pct = int(round(100 * float(score) / float(total)))
227
+ st.caption(f"Last score: {pct}%")
228
+ except Exception:
229
+ st.caption("Last score: —")
230
  else:
231
  st.caption("No submission yet")
232
 
233
+ # pass quiz & assignment to the Quiz page
234
+ if st.button("📝 Start Quiz", key=f"start_quiz_{class_id}_{quiz_id}"):
235
+ st.session_state.selected_quiz = quiz_id
236
+ st.session_state.current_assignment = r.get("assignment_id")
237
  st.session_state.current_page = "Quiz"
238
  st.rerun()
239
 
phase/Teacher_view/classmanage.py CHANGED
@@ -1,10 +1,13 @@
1
  # phase/Teacher_view/classmanage.py
2
-
3
  import streamlit as st
4
- import random
5
- import string
6
- from datetime import datetime
7
  from utils import db as dbapi
 
 
 
 
 
8
 
9
  def _metric_card(label: str, value: str, caption: str = ""):
10
  st.markdown(
@@ -18,6 +21,23 @@ def _metric_card(label: str, value: str, caption: str = ""):
18
  unsafe_allow_html=True,
19
  )
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def show_page():
22
  user = st.session_state.user
23
  teacher_id = user["user_id"]
@@ -30,29 +50,45 @@ def show_page():
30
  new_name = st.text_input("Classroom Name", key="new_classroom_name")
31
  if st.button("Create Classroom"):
32
  name = new_name.strip()
33
- if name:
34
- out = dbapi.create_class(teacher_id, name)
35
- st.session_state.selected_class_id = out["class_id"]
36
- st.success(f'Classroom "{name}" created with code: {out["code"]}')
37
- st.rerun()
38
- else:
39
  st.error("Enter a real name, not whitespace.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  # -------- Load classes for this teacher --------
42
- classes = dbapi.list_classes_by_teacher(teacher_id)
 
 
 
 
 
 
43
  if not classes:
44
  st.info("No classrooms yet. Create one above, then share the code.")
45
  return
46
 
47
- # Picker like your mock header bar
48
  st.subheader("Your Classrooms")
49
- options = {f"{c['name']} (Code: {c.get('code','')})": c for c in classes}
50
  selected_label = st.selectbox("Select a classroom", list(options.keys()))
51
  selected = options[selected_label]
52
  class_id = selected["class_id"]
53
 
54
  st.markdown("---")
55
- st.header(selected["name"])
56
 
57
  # -------- Code stripe --------
58
  st.subheader("Class Code")
@@ -70,14 +106,18 @@ def show_page():
70
 
71
  # ============== Students tab ==============
72
  with tab_students:
73
- # search input
74
  q = st.text_input("Search students by name or email", "")
75
- roster = dbapi.list_students_in_class(class_id)
 
 
 
 
 
76
 
77
  # simple filter
78
  if q.strip():
79
  ql = q.lower()
80
- roster = [r for r in roster if ql in r["name"].lower() or ql in r["email"].lower()]
81
 
82
  st.caption(f"{len(roster)} Students Found")
83
 
@@ -85,41 +125,65 @@ def show_page():
85
  st.info("No students in this class yet.")
86
  else:
87
  for s in roster:
88
- st.subheader(f"👤 {s['name']}")
89
- st.caption(s["email"])
90
  joined = s.get("joined_at") or s.get("created_at")
91
- st.caption(f"📅 Joined: {str(joined)[:10]}")
92
- st.progress(0.0) # placeholder bar to match your style
93
  cols = st.columns(3)
94
- cols[0].metric("⭐ Level", s["level_slug"].capitalize())
95
- cols[1].metric("📊 Avg Score", "—") # can be filled per-student later
96
- cols[2].metric("🔥 Streak", "—") # from streaks table if you want
 
 
 
 
 
97
  st.markdown("---")
98
 
99
  # ============== Content tab ==============
100
  with tab_content:
101
- counts = dbapi.class_content_counts(class_id)
 
 
 
 
 
102
  left, right = st.columns(2)
103
  with left:
104
- _metric_card("📖 Custom Lessons", str(counts["lessons"]), "Lessons created for this classroom")
105
  with right:
106
- _metric_card("🏆 Custom Quizzes", str(counts["quizzes"]), "Quizzes created for this classroom")
107
-
108
- # Optional list so teachers know what those numbers are
109
- assigs = dbapi.list_class_assignments(class_id)
 
 
 
 
110
  if assigs:
111
  st.markdown("#### Assigned items")
112
  for a in assigs:
113
- has_quiz = " + Quiz" if a["quiz_id"] else ""
114
- st.markdown(f"- **{a['title']}** · {a['subject']} · {a['level']}{has_quiz}")
115
 
116
  # ============== Analytics tab ==============
117
  with tab_analytics:
118
- stats = dbapi.class_analytics(class_id)
 
 
 
 
 
 
 
 
 
 
119
  g1, g2, g3 = st.columns(3)
120
  with g1:
121
- _metric_card("📊 Class Average", f"{round(stats['class_avg']*100)}%", "Average quiz performance")
122
  with g2:
123
- _metric_card("🪙 Total XP", f"{stats['total_xp']}", "Combined XP earned")
124
  with g3:
125
- _metric_card("📘 Lessons Completed", f"{stats['lessons_completed']}", "Total lessons completed")
 
1
  # phase/Teacher_view/classmanage.py
2
+ import os
3
  import streamlit as st
4
+
 
 
5
  from utils import db as dbapi
6
+ import utils.api as api # backend Space client
7
+
8
+ # When DISABLE_DB=1 (default), skip direct MySQL and use backend APIs
9
+ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
10
+
11
 
12
  def _metric_card(label: str, value: str, caption: str = ""):
13
  st.markdown(
 
21
  unsafe_allow_html=True,
22
  )
23
 
24
+
25
+ def _prefer_db(db_name: str, api_func, default, *args, **kwargs):
26
+ """
27
+ Try local DB function if enabled & present; else call backend API; else return default.
28
+ """
29
+ if USE_LOCAL_DB and hasattr(dbapi, db_name):
30
+ try:
31
+ return getattr(dbapi, db_name)(*args, **kwargs)
32
+ except Exception as e:
33
+ st.warning(f"DB call {db_name} failed; falling back to backend. ({e})")
34
+ try:
35
+ return api_func(*args, **kwargs)
36
+ except Exception as e:
37
+ st.error(f"Backend call failed: {e}")
38
+ return default
39
+
40
+
41
  def show_page():
42
  user = st.session_state.user
43
  teacher_id = user["user_id"]
 
50
  new_name = st.text_input("Classroom Name", key="new_classroom_name")
51
  if st.button("Create Classroom"):
52
  name = new_name.strip()
53
+ if not name:
 
 
 
 
 
54
  st.error("Enter a real name, not whitespace.")
55
+ else:
56
+ # positional args first, then keywords (we only use positional here)
57
+ out = _prefer_db(
58
+ "create_class",
59
+ lambda tid, n: api.create_class(tid, n),
60
+ None,
61
+ teacher_id,
62
+ name,
63
+ )
64
+ if out:
65
+ st.session_state.selected_class_id = out.get("class_id")
66
+ st.success(f'Classroom "{name}" created with code: {out.get("code","—")}')
67
+ st.rerun()
68
+ else:
69
+ st.error("Could not create classroom (no response).")
70
 
71
  # -------- Load classes for this teacher --------
72
+ classes = _prefer_db(
73
+ "list_classes_by_teacher",
74
+ lambda tid: api.list_classes_by_teacher(tid),
75
+ [],
76
+ teacher_id, # positional
77
+ )
78
+
79
  if not classes:
80
  st.info("No classrooms yet. Create one above, then share the code.")
81
  return
82
 
83
+ # Picker
84
  st.subheader("Your Classrooms")
85
+ options = {f"{c.get('name','(unnamed)')} (Code: {c.get('code','')})": c for c in classes}
86
  selected_label = st.selectbox("Select a classroom", list(options.keys()))
87
  selected = options[selected_label]
88
  class_id = selected["class_id"]
89
 
90
  st.markdown("---")
91
+ st.header(selected.get("name", "Classroom"))
92
 
93
  # -------- Code stripe --------
94
  st.subheader("Class Code")
 
106
 
107
  # ============== Students tab ==============
108
  with tab_students:
 
109
  q = st.text_input("Search students by name or email", "")
110
+ roster = _prefer_db(
111
+ "list_students_in_class",
112
+ lambda cid: api.list_students_in_class(cid),
113
+ [],
114
+ class_id, # positional
115
+ )
116
 
117
  # simple filter
118
  if q.strip():
119
  ql = q.lower()
120
+ roster = [r for r in roster if ql in (r.get("name","").lower()) or ql in (r.get("email","").lower())]
121
 
122
  st.caption(f"{len(roster)} Students Found")
123
 
 
125
  st.info("No students in this class yet.")
126
  else:
127
  for s in roster:
128
+ st.subheader(f"👤 {s.get('name','(unknown)')}")
129
+ st.caption(s.get("email","—"))
130
  joined = s.get("joined_at") or s.get("created_at")
131
+ st.caption(f"📅 Joined: {str(joined)[:10] if joined else '—'}")
132
+ st.progress(0.0) # placeholder bar
133
  cols = st.columns(3)
134
+ level_slug = (s.get("level_slug") or s.get("level") or "beginner")
135
+ try:
136
+ level_label = level_slug.capitalize() if isinstance(level_slug, str) else str(level_slug)
137
+ except Exception:
138
+ level_label = "—"
139
+ cols[0].metric("⭐ Level", level_label)
140
+ cols[1].metric("📊 Avg Score", "—")
141
+ cols[2].metric("🔥 Streak", "—")
142
  st.markdown("---")
143
 
144
  # ============== Content tab ==============
145
  with tab_content:
146
+ counts = _prefer_db(
147
+ "class_content_counts",
148
+ lambda cid: api.class_content_counts(cid),
149
+ {"lessons": 0, "quizzes": 0},
150
+ class_id, # positional
151
+ )
152
  left, right = st.columns(2)
153
  with left:
154
+ _metric_card("📖 Custom Lessons", str(counts.get("lessons", 0)), "Lessons created for this classroom")
155
  with right:
156
+ _metric_card("🏆 Custom Quizzes", str(counts.get("quizzes", 0)), "Quizzes created for this classroom")
157
+
158
+ assigs = _prefer_db(
159
+ "list_class_assignments",
160
+ lambda cid: api.list_class_assignments(cid),
161
+ [],
162
+ class_id, # positional
163
+ )
164
  if assigs:
165
  st.markdown("#### Assigned items")
166
  for a in assigs:
167
+ has_quiz = " + Quiz" if a.get("quiz_id") else ""
168
+ st.markdown(f"- **{a.get('title','Untitled')}** · {a.get('subject','—')} · {a.get('level','—')}{has_quiz}")
169
 
170
  # ============== Analytics tab ==============
171
  with tab_analytics:
172
+ stats = _prefer_db(
173
+ "class_analytics",
174
+ lambda cid: api.class_analytics(cid),
175
+ {"class_avg": 0.0, "total_xp": 0, "lessons_completed": 0},
176
+ class_id, # positional
177
+ )
178
+
179
+ class_avg_pct = round(float(stats.get("class_avg", 0)) * 100) if stats.get("class_avg") is not None else 0
180
+ total_xp = stats.get("total_xp", 0)
181
+ lessons_completed = stats.get("lessons_completed", 0)
182
+
183
  g1, g2, g3 = st.columns(3)
184
  with g1:
185
+ _metric_card("📊 Class Average", f"{class_avg_pct}%", "Average quiz performance")
186
  with g2:
187
+ _metric_card("🪙 Total XP", f"{total_xp}", "Combined XP earned")
188
  with g3:
189
+ _metric_card("📘 Lessons Completed", f"{lessons_completed}", "Total lessons completed")
phase/Teacher_view/contentmanage.py CHANGED
@@ -1,8 +1,13 @@
1
  # phase/Teacher_view/contentmanage.py
2
  import json
3
- import streamlit as st
4
  from datetime import datetime
 
5
  from utils import db as dbapi
 
 
 
 
6
 
7
  # ---------- small UI helpers ----------
8
  def _pill(text):
@@ -16,36 +21,28 @@ def _progress(val: float):
16
  </div>
17
  """
18
 
 
 
 
 
 
 
 
 
19
 
20
- # ---------- OpenAI quiz generator ----------
21
- def _generate_quiz_from_text(content: str, n_questions: int = 5):
22
  """
23
- Returns a list of dicts like:
24
- {"question": "...", "options": ["A","B","C","D"], "answer_key": "B", "points": 1}
25
- Uses OPENAI_API_KEY from your env.
26
  """
27
- system = (
28
- "You are a Jamaican primary school financial literacy teacher. "
29
- "Write clear multiple-choice questions (A-D) about the provided lesson content. "
30
- "Keep language simple and age-appropriate. Only one correct answer per question."
31
- )
32
- user = (
33
- f"Create {n_questions} MCQs strictly in this JSON format:\n"
34
- "{\n"
35
- ' \"items\":[\n'
36
- ' {\"question\":\"...\", \"options\":[\"A\",\"B\",\"C\",\"D\"], \"answer_key\":\"A\"}\n'
37
- " ]\n"
38
- "}\n\n"
39
- "Lesson content:\n"
40
- f"{content}"
41
- )
42
-
43
  def _normalize(items):
44
  out = []
45
  for it in (items or [])[:n_questions]:
46
  q = str(it.get("question", "")).strip()
47
  opts = it.get("options", [])
48
- if not q or not isinstance(opts, list) or len(opts) < 2:
49
  continue
50
  while len(opts) < 4:
51
  opts.append("Option")
@@ -57,53 +54,108 @@ def _generate_quiz_from_text(content: str, n_questions: int = 5):
57
  return out
58
 
59
  try:
60
- from openai import OpenAI
61
- client = OpenAI()
62
-
63
- # 1) Preferred path: Responses API
64
- try:
65
- resp = client.responses.create(
66
- model="gpt-4o-mini",
67
- temperature=0.2,
68
- response_format={"type": "json_object"},
69
- input=[
70
- {"role": "system", "content": [{"type": "text", "text": system}]},
71
- {"role": "user", "content": [{"type": "text", "text": user}]},
72
- ],
73
- )
74
- raw = getattr(resp, "output_text", "") or ""
75
- data = json.loads(raw)
76
- return _normalize(data.get("items", []))
77
-
78
- # 2) Fallback: Chat Completions
79
- except Exception:
80
- resp = client.chat.completions.create(
81
- model="gpt-4o-mini",
82
- temperature=0.2,
83
- messages=[{"role":"system","content":system},{"role":"user","content":user}],
84
- response_format={"type": "json_object"},
85
- )
86
- raw = resp.choices[0].message.content.strip()
87
- data = json.loads(raw)
88
- return _normalize(data.get("items", []))
89
-
90
  except Exception as e:
91
  with st.expander("Quiz generation error details"):
92
  st.code(str(e))
93
- st.warning("Quiz generation failed. Check API key and your openai package version.")
94
  return []
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  # ---------- Create panels ----------
97
  def _create_lesson_panel(teacher_id: int):
98
  st.markdown("### ✍️ Create New Lesson")
99
 
100
- classes = dbapi.list_classes_by_teacher(teacher_id)
101
  class_opts = {f"{c['name']} (code {c['code']})": c["class_id"] for c in classes} if classes else {}
102
 
103
  if "cl_topic_count" not in st.session_state:
104
  st.session_state.cl_topic_count = 2 # start with two topics
105
 
106
- # UI-manipulation buttons OUTSIDE the form
107
  cols_btn = st.columns([1,1,6])
108
  with cols_btn[0]:
109
  if st.button("➕ Add topic", type="secondary"):
@@ -144,16 +196,13 @@ def _create_lesson_panel(teacher_id: int):
144
 
145
  st.markdown("#### Auto-generate a quiz from this lesson (optional)")
146
  gen_quiz = st.checkbox("Generate a quiz from content", value=False)
147
- q_count = st.slider("", 3, 10, 5) # label empty because already described
148
-
149
- # ONLY keep the main submit button inside the form
150
- submitted = st.form_submit_button("Create lesson", type="primary")
151
 
 
152
 
153
  if not submitted:
154
  return
155
 
156
- # build sections payload for DB
157
  sections = []
158
  for t, b in topic_rows:
159
  if (t or b):
@@ -169,41 +218,47 @@ def _create_lesson_panel(teacher_id: int):
169
  st.error("Please add a title and at least one topic.")
170
  return
171
 
172
- # create lesson
173
- lesson_id = dbapi.create_lesson(teacher_id, title, description, subject, level, sections)
174
- st.success(f"✅ Lesson created (ID {lesson_id}).")
 
 
 
 
175
 
176
  # assign to chosen classes (lesson only for now)
177
  for label in assign_classes:
178
- dbapi.assign_to_class(lesson_id, None, class_opts[label], teacher_id)
 
 
 
179
 
180
- # auto-generate quiz
181
  if gen_quiz:
182
  text = "\n\n".join([s["title"] + "\n" + (s["content"] or "") for s in sections])
183
- with st.spinner("Generating quiz..."):
184
- items = _generate_quiz_from_text(text, n_questions=q_count)
185
  if items:
186
- qid = dbapi.create_quiz(lesson_id, f"{title} - Quiz", items, {})
187
- st.success(f"🧠 Quiz generated and saved (ID {qid}).")
188
- for label in assign_classes:
189
- dbapi.assign_to_class(lesson_id, qid, class_opts[label], teacher_id)
 
 
 
190
 
191
  st.session_state.show_create_lesson = False
192
  st.rerun()
193
 
194
-
195
-
196
  def _create_quiz_panel(teacher_id: int):
197
  st.markdown("### 🏆 Create New Quiz")
198
 
199
- # teacher lessons to link
200
- lessons = dbapi.list_lessons_by_teacher(teacher_id)
201
  lesson_map = {f"{L['title']} (#{L['lesson_id']})": L["lesson_id"] for L in lessons}
202
  if not lesson_map:
203
  st.info("Create a lesson first, then link a quiz to it.")
204
  return
205
 
206
- # dynamic questions
207
  if "cq_q_count" not in st.session_state:
208
  st.session_state.cq_q_count = 5
209
 
@@ -244,7 +299,6 @@ def _create_quiz_panel(teacher_id: int):
244
  st.error("Please add a quiz title.")
245
  return
246
 
247
- # sanitize items
248
  cleaned = []
249
  for it in items:
250
  q = (it["question"] or "").strip()
@@ -259,17 +313,23 @@ def _create_quiz_panel(teacher_id: int):
259
  st.error("Add at least one valid question.")
260
  return
261
 
262
- qid = dbapi.create_quiz(lesson_map[lesson_label], title, cleaned, {})
263
- st.success(f"✅ Quiz created (ID {qid}).")
264
-
265
- st.session_state.show_create_quiz = False
266
- st.rerun()
267
-
 
268
 
269
  def _edit_lesson_panel(teacher_id: int, lesson_id: int):
270
- data = dbapi.get_lesson(lesson_id)
271
- L = data["lesson"]
272
- secs = data["sections"] or []
 
 
 
 
 
273
 
274
  key_cnt = f"el_cnt_{lesson_id}"
275
  if key_cnt not in st.session_state:
@@ -277,7 +337,6 @@ def _edit_lesson_panel(teacher_id: int, lesson_id: int):
277
 
278
  st.markdown("### ✏️ Edit Lesson")
279
 
280
- #Move UI-manipulation buttons
281
  tools = st.columns([1,1,8])
282
  with tools[0]:
283
  if st.button("➕ Add section", key=f"el_add_{lesson_id}", use_container_width=True):
@@ -289,17 +348,16 @@ def _edit_lesson_panel(teacher_id: int, lesson_id: int):
289
  st.session_state[key_cnt] = max(1, st.session_state[key_cnt] - 1)
290
  st.rerun()
291
 
292
- # The form only has fields + a single submit (Save)
293
  with st.form(f"edit_lesson_form_{lesson_id}", clear_on_submit=False):
294
  c1, c2 = st.columns([2,1])
295
- title = c1.text_input("Title", value=L["title"])
296
  level = c2.selectbox(
297
  "Level",
298
  ["beginner","intermediate","advanced"],
299
- index=["beginner","intermediate","advanced"].index(L["level"])
300
  )
301
  description = st.text_area("Short description", value=L.get("description") or "")
302
- subject = st.selectbox("Subject", ["numeracy","finance"], index=(0 if L["subject"]=="numeracy" else 1))
303
 
304
  st.markdown("#### Sections")
305
  edited_sections = []
@@ -313,7 +371,6 @@ def _edit_lesson_panel(teacher_id: int, lesson_id: int):
313
 
314
  save = st.form_submit_button("💾 Save changes", type="primary", use_container_width=True)
315
 
316
- # Cancel is a normal button outside the form
317
  actions = st.columns([8,2])
318
  with actions[1]:
319
  cancel_clicked = st.button("✖ Cancel", key=f"el_cancel_{lesson_id}", type="secondary", use_container_width=True)
@@ -326,29 +383,37 @@ def _edit_lesson_panel(teacher_id: int, lesson_id: int):
326
  if not save:
327
  return
328
 
329
- # validation + persist
330
  if not title or not any((s["title"] or s["content"]).strip() for s in edited_sections):
331
  st.error("Title and at least one non-empty section are required.")
332
  return
333
 
334
- ok = dbapi.update_lesson(lesson_id, teacher_id, title, description, subject, level, edited_sections)
 
 
 
 
 
335
  if ok:
336
  st.success("✅ Lesson updated.")
337
  st.session_state.show_edit_lesson = False
338
  st.session_state.edit_lesson_id = None
339
  st.rerun()
340
  else:
341
- st.error("Could not update this lesson. Check ownership or DB errors.")
342
-
343
 
344
  def _edit_quiz_panel(teacher_id: int, quiz_id: int):
345
- data = dbapi.get_quiz(quiz_id) # {'quiz': {...}, 'items': [...]}
346
- if not data or not data.get("quiz"):
347
- st.error("Quiz not found.")
 
 
348
  return
349
 
350
- Q = data["quiz"]
351
  raw_items = data.get("items", [])
 
 
 
352
 
353
  def _dec(x):
354
  if isinstance(x, str):
@@ -358,7 +423,6 @@ def _edit_quiz_panel(teacher_id: int, quiz_id: int):
358
  return x
359
  return x
360
 
361
- # Normalize into simple dicts that the form can bind to
362
  items = []
363
  for it in raw_items:
364
  opts = _dec(it.get("options")) or []
@@ -427,7 +491,6 @@ def _edit_quiz_panel(teacher_id: int, quiz_id: int):
427
  if not save:
428
  return
429
 
430
- # sanitize
431
  cleaned = []
432
  for it in edited:
433
  q = (it["question"] or "").strip()
@@ -439,7 +502,7 @@ def _edit_quiz_panel(teacher_id: int, quiz_id: int):
439
  cleaned.append({
440
  "question": q,
441
  "options": opts[:4],
442
- "answer_key": it["answer_key"], # single letter
443
  "points": 1
444
  })
445
 
@@ -447,15 +510,19 @@ def _edit_quiz_panel(teacher_id: int, quiz_id: int):
447
  st.error("Title and at least one valid question are required.")
448
  return
449
 
450
- ok = dbapi.update_quiz(quiz_id, teacher_id, title, cleaned, settings={})
 
 
 
 
 
451
  if ok:
452
  st.success("✅ Quiz updated.")
453
  st.session_state.show_edit_quiz = False
454
  st.session_state.edit_quiz_id = None
455
  st.rerun()
456
  else:
457
- st.error("Could not update this quiz. Check ownership or DB errors.")
458
-
459
 
460
  # ---------- Main page ----------
461
  def show_page():
@@ -466,17 +533,17 @@ def show_page():
466
  st.caption("Create and manage custom lessons and quizzes")
467
 
468
  # preload lists
469
- lessons = dbapi.list_lessons_by_teacher(teacher_id)
470
- quizzes = dbapi.list_quizzes_by_teacher(teacher_id)
471
 
472
- # top action bar (no popovers)
473
  a1, a2, _sp = st.columns([3,3,4])
474
  if a1.button("➕ Create Lesson", use_container_width=True):
475
  st.session_state.show_create_lesson = True
476
  if a2.button("🏆 Create Quiz", use_container_width=True):
477
  st.session_state.show_create_quiz = True
478
 
479
- # big inline create panels
480
  if st.session_state.get("show_create_lesson"):
481
  with st.container(border=True):
482
  _create_lesson_panel(teacher_id)
@@ -487,18 +554,17 @@ def show_page():
487
  _create_quiz_panel(teacher_id)
488
  st.markdown("---")
489
 
490
- # ----- Inline lesson edit panel, when triggered -----
491
  if st.session_state.get("show_edit_lesson") and st.session_state.get("edit_lesson_id"):
492
  with st.container(border=True):
493
  _edit_lesson_panel(teacher_id, st.session_state.edit_lesson_id)
494
  st.markdown("---")
495
- # ----- Inline quiz edit panel, when triggered -----
496
  if st.session_state.get("show_edit_quiz") and st.session_state.get("edit_quiz_id"):
497
  with st.container(border=True):
498
  _edit_quiz_panel(teacher_id, st.session_state.edit_quiz_id)
499
  st.markdown("---")
500
 
501
-
502
  # Tabs
503
  tab1, tab2 = st.tabs([f"Custom Lessons ({len(lessons)})", f"Custom Quizzes ({len(quizzes)})"])
504
 
@@ -507,14 +573,13 @@ def show_page():
507
  if not lessons:
508
  st.info("No lessons yet. Use **Create Lesson** above.")
509
  else:
510
- # all students across teacher's classes (optional “assign to students” inline UI you already had)
511
- all_students = dbapi.list_all_students_for_teacher(teacher_id)
512
  student_options = {f"{s['name']} · {s['email']}": s["user_id"] for s in all_students}
513
 
514
  for L in lessons:
515
- assignees = dbapi.list_assigned_students_for_lesson(L["lesson_id"])
516
- assignee_names = [a["name"] for a in assignees]
517
- created = L["created_at"].strftime("%Y-%m-%d") if isinstance(L["created_at"], datetime) else str(L["created_at"])[:10]
518
  count = len(assignees)
519
 
520
  with st.container(border=True):
@@ -523,8 +588,8 @@ def show_page():
523
  st.markdown(f"### {L['title']}")
524
  st.caption(L.get("description") or "")
525
  st.markdown(
526
- _pill(L["level"].capitalize()) +
527
- _pill(L["subject"]) +
528
  _pill(f"{count} student{'s' if count != 1 else ''} assigned") +
529
  _pill(f"Created {created}"),
530
  unsafe_allow_html=True
@@ -538,13 +603,13 @@ def show_page():
538
  st.rerun()
539
  with b2:
540
  if st.button("Delete", key=f"del_{L['lesson_id']}"):
541
- ok, msg = dbapi.delete_lesson(L["lesson_id"], teacher_id)
542
  if ok: st.success("Lesson deleted"); st.rerun()
543
- else: st.error(msg)
544
 
545
  st.markdown("**Assigned Students:**")
546
  if assignee_names:
547
- st.markdown(" ".join(_pill(n) for n in assignee_names), unsafe_allow_html=True)
548
  else:
549
  st.caption("No students assigned yet.")
550
 
@@ -554,15 +619,15 @@ def show_page():
554
  st.info("No quizzes yet. Use **Create Quiz** above.")
555
  else:
556
  for Q in quizzes:
557
- assignees = dbapi.list_assigned_students_for_quiz(Q["quiz_id"])
558
- created = Q["created_at"].strftime("%Y-%m-%d") if isinstance(Q["created_at"], datetime) else str(Q["created_at"])[:10]
559
  num_qs = int(Q.get("num_items", 0))
560
 
561
  with st.container(border=True):
562
  c1, c2 = st.columns([8,3])
563
  with c1:
564
  st.markdown(f"### {Q['title']}")
565
- st.caption(f"Lesson: {Q['lesson_title']}")
566
  st.markdown(
567
  _pill(f"{num_qs} question{'s' if num_qs != 1 else ''}") +
568
  _pill(f"{len(assignees)} students assigned") +
@@ -578,25 +643,30 @@ def show_page():
578
  st.rerun()
579
  with b2:
580
  if st.button("Delete", key=f"delq_{Q['quiz_id']}"):
581
- ok, msg = dbapi.delete_quiz(Q["quiz_id"], teacher_id)
582
  if ok: st.success("Quiz deleted"); st.rerun()
583
- else: st.error(msg)
584
 
585
  st.markdown("**Assigned Students:**")
586
  if assignees:
587
- st.markdown(" ".join(_pill(a['name']) for a in assignees), unsafe_allow_html=True)
588
  else:
589
  st.caption("No students assigned yet.")
590
 
591
  with st.expander("View questions", expanded=False):
592
- data = dbapi.get_quiz(Q["quiz_id"]) # {'quiz': {...}, 'items': [...]}
 
 
 
 
 
 
593
  items = data.get("items", []) if data else []
594
  if not items:
595
  st.info("No items found for this quiz.")
596
  else:
597
  labels = ["A","B","C","D"]
598
  for i, it in enumerate(items, start=1):
599
- # Handle JSON columns that may come back as strings
600
  opts = it.get("options")
601
  if isinstance(opts, str):
602
  try:
@@ -615,4 +685,4 @@ def show_page():
615
  st.write(f"{labels[j]}) {opt}")
616
  ans_text = answer if isinstance(answer, str) else ",".join(answer or [])
617
  st.caption(f"Answer: {ans_text}")
618
- st.markdown("---")
 
1
  # phase/Teacher_view/contentmanage.py
2
  import json
3
+ import os
4
  from datetime import datetime
5
+ import streamlit as st
6
  from utils import db as dbapi
7
+ import utils.api as api # backend Space client
8
+
9
+ # Switch automatically: if DISABLE_DB=1 (default), use backend API; else use local DB
10
+ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
11
 
12
  # ---------- small UI helpers ----------
13
  def _pill(text):
 
21
  </div>
22
  """
23
 
24
+ def _fmt_date(v):
25
+ if isinstance(v, datetime):
26
+ return v.strftime("%Y-%m-%d")
27
+ try:
28
+ s = str(v)
29
+ return s[:10]
30
+ except Exception:
31
+ return ""
32
 
33
+ # ---------- Quiz generator via backend LLM (llama 3.1 8B) ----------
34
+ def _generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "finance", level: str = "beginner"):
35
  """
36
+ Calls your backend, which uses GEN_MODEL (llama-3.1-8b-instruct).
37
+ Returns a normalized list like:
38
+ [{"question":"...","options":["A","B","C","D"],"answer_key":"B","points":1}, ...]
39
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def _normalize(items):
41
  out = []
42
  for it in (items or [])[:n_questions]:
43
  q = str(it.get("question", "")).strip()
44
  opts = it.get("options", [])
45
+ if not q or not isinstance(opts, list):
46
  continue
47
  while len(opts) < 4:
48
  opts.append("Option")
 
54
  return out
55
 
56
  try:
57
+ resp = api.generate_quiz_from_text(content, n_questions=n_questions, subject=subject, level=level)
58
+ items = resp.get("items", resp) # allow backend to return either shape
59
+ return _normalize(items)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  except Exception as e:
61
  with st.expander("Quiz generation error details"):
62
  st.code(str(e))
63
+ st.warning("Quiz generation failed via backend. Check the /quiz/generate endpoint and GEN_MODEL.")
64
  return []
65
 
66
+ # ---------- Thin wrappers that choose DB or Backend ----------
67
+ def _list_classes_by_teacher(teacher_id: int):
68
+ if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"):
69
+ return dbapi.list_classes_by_teacher(teacher_id)
70
+ try:
71
+ return api.list_classes_by_teacher(teacher_id)
72
+ except Exception:
73
+ return []
74
+
75
+ def _list_all_students_for_teacher(teacher_id: int):
76
+ if USE_LOCAL_DB and hasattr(dbapi, "list_all_students_for_teacher"):
77
+ return dbapi.list_all_students_for_teacher(teacher_id)
78
+ try:
79
+ return api.list_all_students_for_teacher(teacher_id)
80
+ except Exception:
81
+ return []
82
+
83
+ def _list_lessons_by_teacher(teacher_id: int):
84
+ if USE_LOCAL_DB and hasattr(dbapi, "list_lessons_by_teacher"):
85
+ return dbapi.list_lessons_by_teacher(teacher_id)
86
+ try:
87
+ return api.list_lessons_by_teacher(teacher_id)
88
+ except Exception:
89
+ return []
90
+
91
+ def _list_quizzes_by_teacher(teacher_id: int):
92
+ if USE_LOCAL_DB and hasattr(dbapi, "list_quizzes_by_teacher"):
93
+ return dbapi.list_quizzes_by_teacher(teacher_id)
94
+ try:
95
+ return api.list_quizzes_by_teacher(teacher_id)
96
+ except Exception:
97
+ return []
98
+
99
+ def _create_lesson(teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
100
+ if USE_LOCAL_DB and hasattr(dbapi, "create_lesson"):
101
+ return dbapi.create_lesson(teacher_id, title, description, subject, level, sections)
102
+ return api.create_lesson(teacher_id, title, description, subject, level, sections)
103
+
104
+ def _update_lesson(lesson_id: int, teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
105
+ if USE_LOCAL_DB and hasattr(dbapi, "update_lesson"):
106
+ return dbapi.update_lesson(lesson_id, teacher_id, title, description, subject, level, sections)
107
+ return api.update_lesson(lesson_id, teacher_id, title, description, subject, level, sections)
108
+
109
+ def _delete_lesson(lesson_id: int, teacher_id: int):
110
+ if USE_LOCAL_DB and hasattr(dbapi, "delete_lesson"):
111
+ return dbapi.delete_lesson(lesson_id, teacher_id)
112
+ return api.delete_lesson(lesson_id, teacher_id)
113
+
114
+ def _get_lesson(lesson_id: int):
115
+ if USE_LOCAL_DB and hasattr(dbapi, "get_lesson"):
116
+ return dbapi.get_lesson(lesson_id)
117
+ return api.get_lesson(lesson_id)
118
+
119
+ def _create_quiz(lesson_id: int, title: str, items: list[dict], settings: dict):
120
+ if USE_LOCAL_DB and hasattr(dbapi, "create_quiz"):
121
+ return dbapi.create_quiz(lesson_id, title, items, settings)
122
+ return api.create_quiz(lesson_id, title, items, settings)
123
+
124
+ def _update_quiz(quiz_id: int, teacher_id: int, title: str, items: list[dict], settings: dict):
125
+ if USE_LOCAL_DB and hasattr(dbapi, "update_quiz"):
126
+ return dbapi.update_quiz(quiz_id, teacher_id, title, items, settings)
127
+ return api.update_quiz(quiz_id, teacher_id, title, items, settings)
128
+
129
+ def _delete_quiz(quiz_id: int, teacher_id: int):
130
+ if USE_LOCAL_DB and hasattr(dbapi, "delete_quiz"):
131
+ return dbapi.delete_quiz(quiz_id, teacher_id)
132
+ return api.delete_quiz(quiz_id, teacher_id)
133
+
134
+ def _list_assigned_students_for_lesson(lesson_id: int):
135
+ if USE_LOCAL_DB and hasattr(dbapi, "list_assigned_students_for_lesson"):
136
+ return dbapi.list_assigned_students_for_lesson(lesson_id)
137
+ return api.list_assigned_students_for_lesson(lesson_id)
138
+
139
+ def _list_assigned_students_for_quiz(quiz_id: int):
140
+ if USE_LOCAL_DB and hasattr(dbapi, "list_assigned_students_for_quiz"):
141
+ return dbapi.list_assigned_students_for_quiz(quiz_id)
142
+ return api.list_assigned_students_for_quiz(quiz_id)
143
+
144
+ def _assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, teacher_id: int):
145
+ if USE_LOCAL_DB and hasattr(dbapi, "assign_to_class"):
146
+ return dbapi.assign_to_class(lesson_id, quiz_id, class_id, teacher_id)
147
+ return api.assign_to_class(lesson_id, quiz_id, class_id, teacher_id)
148
+
149
  # ---------- Create panels ----------
150
  def _create_lesson_panel(teacher_id: int):
151
  st.markdown("### ✍️ Create New Lesson")
152
 
153
+ classes = _list_classes_by_teacher(teacher_id)
154
  class_opts = {f"{c['name']} (code {c['code']})": c["class_id"] for c in classes} if classes else {}
155
 
156
  if "cl_topic_count" not in st.session_state:
157
  st.session_state.cl_topic_count = 2 # start with two topics
158
 
 
159
  cols_btn = st.columns([1,1,6])
160
  with cols_btn[0]:
161
  if st.button("➕ Add topic", type="secondary"):
 
196
 
197
  st.markdown("#### Auto-generate a quiz from this lesson (optional)")
198
  gen_quiz = st.checkbox("Generate a quiz from content", value=False)
199
+ q_count = st.slider("", 3, 10, 5)
 
 
 
200
 
201
+ submitted = st.form_submit_button("Create lesson", type="primary")
202
 
203
  if not submitted:
204
  return
205
 
 
206
  sections = []
207
  for t, b in topic_rows:
208
  if (t or b):
 
218
  st.error("Please add a title and at least one topic.")
219
  return
220
 
221
+ # create lesson (DB or backend)
222
+ try:
223
+ lesson_id = _create_lesson(teacher_id, title, description, subject, level, sections)
224
+ st.success(f"✅ Lesson created (ID {lesson_id}).")
225
+ except Exception as e:
226
+ st.error(f"Failed to create lesson: {e}")
227
+ return
228
 
229
  # assign to chosen classes (lesson only for now)
230
  for label in assign_classes:
231
+ try:
232
+ _assign_to_class(lesson_id, None, class_opts[label], teacher_id)
233
+ except Exception as e:
234
+ st.warning(f"Could not assign to {label}: {e}")
235
 
236
+ # auto-generate quiz via backend LLM
237
  if gen_quiz:
238
  text = "\n\n".join([s["title"] + "\n" + (s["content"] or "") for s in sections])
239
+ with st.spinner("Generating quiz from lesson content..."):
240
+ items = _generate_quiz_from_text(text, n_questions=q_count, subject=subject, level=level)
241
  if items:
242
+ try:
243
+ qid = _create_quiz(lesson_id, f"{title} - Quiz", items, {})
244
+ st.success(f"🧠 Quiz generated and saved (ID {qid}).")
245
+ for label in assign_classes:
246
+ _assign_to_class(lesson_id, qid, class_opts[label], teacher_id)
247
+ except Exception as e:
248
+ st.warning(f"Lesson saved, but failed to save quiz: {e}")
249
 
250
  st.session_state.show_create_lesson = False
251
  st.rerun()
252
 
 
 
253
  def _create_quiz_panel(teacher_id: int):
254
  st.markdown("### 🏆 Create New Quiz")
255
 
256
+ lessons = _list_lessons_by_teacher(teacher_id)
 
257
  lesson_map = {f"{L['title']} (#{L['lesson_id']})": L["lesson_id"] for L in lessons}
258
  if not lesson_map:
259
  st.info("Create a lesson first, then link a quiz to it.")
260
  return
261
 
 
262
  if "cq_q_count" not in st.session_state:
263
  st.session_state.cq_q_count = 5
264
 
 
299
  st.error("Please add a quiz title.")
300
  return
301
 
 
302
  cleaned = []
303
  for it in items:
304
  q = (it["question"] or "").strip()
 
313
  st.error("Add at least one valid question.")
314
  return
315
 
316
+ try:
317
+ qid = _create_quiz(lesson_map[lesson_label], title, cleaned, {})
318
+ st.success(f"✅ Quiz created (ID {qid}).")
319
+ st.session_state.show_create_quiz = False
320
+ st.rerun()
321
+ except Exception as e:
322
+ st.error(f"Failed to create quiz: {e}")
323
 
324
  def _edit_lesson_panel(teacher_id: int, lesson_id: int):
325
+ try:
326
+ data = _get_lesson(lesson_id)
327
+ except Exception as e:
328
+ st.error(f"Could not load lesson #{lesson_id}: {e}")
329
+ return
330
+
331
+ L = data.get("lesson", {})
332
+ secs = data.get("sections", []) or []
333
 
334
  key_cnt = f"el_cnt_{lesson_id}"
335
  if key_cnt not in st.session_state:
 
337
 
338
  st.markdown("### ✏️ Edit Lesson")
339
 
 
340
  tools = st.columns([1,1,8])
341
  with tools[0]:
342
  if st.button("➕ Add section", key=f"el_add_{lesson_id}", use_container_width=True):
 
348
  st.session_state[key_cnt] = max(1, st.session_state[key_cnt] - 1)
349
  st.rerun()
350
 
 
351
  with st.form(f"edit_lesson_form_{lesson_id}", clear_on_submit=False):
352
  c1, c2 = st.columns([2,1])
353
+ title = c1.text_input("Title", value=L.get("title") or "")
354
  level = c2.selectbox(
355
  "Level",
356
  ["beginner","intermediate","advanced"],
357
+ index=["beginner","intermediate","advanced"].index(L.get("level") or "beginner")
358
  )
359
  description = st.text_area("Short description", value=L.get("description") or "")
360
+ subject = st.selectbox("Subject", ["numeracy","finance"], index=(0 if (L.get("subject")=="numeracy") else 1))
361
 
362
  st.markdown("#### Sections")
363
  edited_sections = []
 
371
 
372
  save = st.form_submit_button("💾 Save changes", type="primary", use_container_width=True)
373
 
 
374
  actions = st.columns([8,2])
375
  with actions[1]:
376
  cancel_clicked = st.button("✖ Cancel", key=f"el_cancel_{lesson_id}", type="secondary", use_container_width=True)
 
383
  if not save:
384
  return
385
 
 
386
  if not title or not any((s["title"] or s["content"]).strip() for s in edited_sections):
387
  st.error("Title and at least one non-empty section are required.")
388
  return
389
 
390
+ ok = False
391
+ try:
392
+ ok = _update_lesson(lesson_id, teacher_id, title, description, subject, level, edited_sections)
393
+ except Exception as e:
394
+ st.error(f"Update failed: {e}")
395
+
396
  if ok:
397
  st.success("✅ Lesson updated.")
398
  st.session_state.show_edit_lesson = False
399
  st.session_state.edit_lesson_id = None
400
  st.rerun()
401
  else:
402
+ st.error("Could not update this lesson. Check ownership or backend errors.")
 
403
 
404
  def _edit_quiz_panel(teacher_id: int, quiz_id: int):
405
+ # Load quiz
406
+ try:
407
+ data = (dbapi.get_quiz(quiz_id) if (USE_LOCAL_DB and hasattr(dbapi, "get_quiz")) else api._req("GET", f"/quizzes/{quiz_id}").json())
408
+ except Exception as e:
409
+ st.error(f"Quiz not found: {e}")
410
  return
411
 
412
+ Q = data.get("quiz")
413
  raw_items = data.get("items", [])
414
+ if not Q:
415
+ st.error("Quiz not found.")
416
+ return
417
 
418
  def _dec(x):
419
  if isinstance(x, str):
 
423
  return x
424
  return x
425
 
 
426
  items = []
427
  for it in raw_items:
428
  opts = _dec(it.get("options")) or []
 
491
  if not save:
492
  return
493
 
 
494
  cleaned = []
495
  for it in edited:
496
  q = (it["question"] or "").strip()
 
502
  cleaned.append({
503
  "question": q,
504
  "options": opts[:4],
505
+ "answer_key": it["answer_key"],
506
  "points": 1
507
  })
508
 
 
510
  st.error("Title and at least one valid question are required.")
511
  return
512
 
513
+ ok = False
514
+ try:
515
+ ok = _update_quiz(quiz_id, teacher_id, title, cleaned, settings={})
516
+ except Exception as e:
517
+ st.error(f"Save failed: {e}")
518
+
519
  if ok:
520
  st.success("✅ Quiz updated.")
521
  st.session_state.show_edit_quiz = False
522
  st.session_state.edit_quiz_id = None
523
  st.rerun()
524
  else:
525
+ st.error("Could not update this quiz. Check ownership or backend errors.")
 
526
 
527
  # ---------- Main page ----------
528
  def show_page():
 
533
  st.caption("Create and manage custom lessons and quizzes")
534
 
535
  # preload lists
536
+ lessons = _list_lessons_by_teacher(teacher_id)
537
+ quizzes = _list_quizzes_by_teacher(teacher_id)
538
 
539
+ # top action bar
540
  a1, a2, _sp = st.columns([3,3,4])
541
  if a1.button("➕ Create Lesson", use_container_width=True):
542
  st.session_state.show_create_lesson = True
543
  if a2.button("🏆 Create Quiz", use_container_width=True):
544
  st.session_state.show_create_quiz = True
545
 
546
+ # create panels
547
  if st.session_state.get("show_create_lesson"):
548
  with st.container(border=True):
549
  _create_lesson_panel(teacher_id)
 
554
  _create_quiz_panel(teacher_id)
555
  st.markdown("---")
556
 
557
+ # inline editors
558
  if st.session_state.get("show_edit_lesson") and st.session_state.get("edit_lesson_id"):
559
  with st.container(border=True):
560
  _edit_lesson_panel(teacher_id, st.session_state.edit_lesson_id)
561
  st.markdown("---")
562
+
563
  if st.session_state.get("show_edit_quiz") and st.session_state.get("edit_quiz_id"):
564
  with st.container(border=True):
565
  _edit_quiz_panel(teacher_id, st.session_state.edit_quiz_id)
566
  st.markdown("---")
567
 
 
568
  # Tabs
569
  tab1, tab2 = st.tabs([f"Custom Lessons ({len(lessons)})", f"Custom Quizzes ({len(quizzes)})"])
570
 
 
573
  if not lessons:
574
  st.info("No lessons yet. Use **Create Lesson** above.")
575
  else:
576
+ all_students = _list_all_students_for_teacher(teacher_id)
 
577
  student_options = {f"{s['name']} · {s['email']}": s["user_id"] for s in all_students}
578
 
579
  for L in lessons:
580
+ assignees = _list_assigned_students_for_lesson(L["lesson_id"])
581
+ assignee_names = [a.get("name") for a in assignees]
582
+ created = _fmt_date(L.get("created_at"))
583
  count = len(assignees)
584
 
585
  with st.container(border=True):
 
588
  st.markdown(f"### {L['title']}")
589
  st.caption(L.get("description") or "")
590
  st.markdown(
591
+ _pill((L.get("level") or "beginner").capitalize()) +
592
+ _pill(L.get("subject","finance")) +
593
  _pill(f"{count} student{'s' if count != 1 else ''} assigned") +
594
  _pill(f"Created {created}"),
595
  unsafe_allow_html=True
 
603
  st.rerun()
604
  with b2:
605
  if st.button("Delete", key=f"del_{L['lesson_id']}"):
606
+ ok, msg = _delete_lesson(L["lesson_id"], teacher_id)
607
  if ok: st.success("Lesson deleted"); st.rerun()
608
+ else: st.error(msg or "Delete failed")
609
 
610
  st.markdown("**Assigned Students:**")
611
  if assignee_names:
612
+ st.markdown(" ".join(_pill(n) for n in assignee_names if n), unsafe_allow_html=True)
613
  else:
614
  st.caption("No students assigned yet.")
615
 
 
619
  st.info("No quizzes yet. Use **Create Quiz** above.")
620
  else:
621
  for Q in quizzes:
622
+ assignees = _list_assigned_students_for_quiz(Q["quiz_id"])
623
+ created = _fmt_date(Q.get("created_at"))
624
  num_qs = int(Q.get("num_items", 0))
625
 
626
  with st.container(border=True):
627
  c1, c2 = st.columns([8,3])
628
  with c1:
629
  st.markdown(f"### {Q['title']}")
630
+ st.caption(f"Lesson: {Q.get('lesson_title','')}")
631
  st.markdown(
632
  _pill(f"{num_qs} question{'s' if num_qs != 1 else ''}") +
633
  _pill(f"{len(assignees)} students assigned") +
 
643
  st.rerun()
644
  with b2:
645
  if st.button("Delete", key=f"delq_{Q['quiz_id']}"):
646
+ ok, msg = _delete_quiz(Q["quiz_id"], teacher_id)
647
  if ok: st.success("Quiz deleted"); st.rerun()
648
+ else: st.error(msg or "Delete failed")
649
 
650
  st.markdown("**Assigned Students:**")
651
  if assignees:
652
+ st.markdown(" ".join(_pill(a.get('name')) for a in assignees if a.get('name')), unsafe_allow_html=True)
653
  else:
654
  st.caption("No students assigned yet.")
655
 
656
  with st.expander("View questions", expanded=False):
657
+ # Load items on demand to avoid heavy initial load
658
+ try:
659
+ data = (dbapi.get_quiz(Q["quiz_id"]) if (USE_LOCAL_DB and hasattr(dbapi, "get_quiz"))
660
+ else api._req("GET", f"/quizzes/{Q['quiz_id']}").json())
661
+ except Exception as e:
662
+ st.info(f"Could not fetch items: {e}")
663
+ data = None
664
  items = data.get("items", []) if data else []
665
  if not items:
666
  st.info("No items found for this quiz.")
667
  else:
668
  labels = ["A","B","C","D"]
669
  for i, it in enumerate(items, start=1):
 
670
  opts = it.get("options")
671
  if isinstance(opts, str):
672
  try:
 
685
  st.write(f"{labels[j]}) {opt}")
686
  ans_text = answer if isinstance(answer, str) else ",".join(answer or [])
687
  st.caption(f"Answer: {ans_text}")
688
+ st.markdown("---")
phase/Teacher_view/studentlist.py CHANGED
@@ -1,36 +1,55 @@
1
  # phase/Teacher_view/studentlist.py
 
2
  import streamlit as st
3
  from utils import db as dbapi
 
 
 
 
4
 
5
  # ---------- tiny helpers ----------
6
  def _avatar(name: str) -> str:
7
-
8
  return "🧑‍🎓" if hash(name) % 2 else "👩‍🎓"
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def _report_text(r, level, avg_pct):
11
  return (
12
  "STUDENT PROGRESS REPORT\n"
13
  "======================\n"
14
- f"Student: {r['name']}\n"
15
- f"Email: {r['email']}\n"
16
- f"Joined: {str(r['joined_at'])[:10]}\n\n"
17
  "PROGRESS OVERVIEW\n"
18
  "-----------------\n"
19
- f"Lessons Completed: {int(r['lessons_completed'] or 0)}/{int(r['total_assigned_lessons'] or 0)}\n"
20
  f"Average Quiz Score: {avg_pct}%\n"
21
- f"Total XP: {int(r['total_xp'] or 0)}\n"
22
  f"Current Level: {level}\n"
23
- f"Study Streak: {int(r['streak_days'] or 0)} days\n"
24
  )
25
 
26
- def _level_from_xp(total_xp: int) -> int:
27
- try:
28
- xp = int(total_xp or 0)
29
- except Exception:
30
- xp = 0
31
- return 1 + xp // 500
32
-
33
-
34
  ROW_CSS = """
35
  <style>
36
  .sm-chip{display:inline-block;padding:4px 10px;border-radius:999px;background:#eef7f1;color:#0b8f5d;font-weight:600;font-size:.80rem;margin-left:8px}
@@ -46,16 +65,56 @@ ROW_CSS = """
46
  </style>
47
  """
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  # ---------- page ----------
50
  def show_page():
51
  st.title("🎓 Student Management")
52
  st.caption("Monitor and manage your students' progress")
53
  st.markdown(ROW_CSS, unsafe_allow_html=True)
54
 
55
- teacher = st.session_state.user
 
 
 
56
  teacher_id = teacher["user_id"]
57
 
58
- classes = dbapi.list_classes_by_teacher(teacher_id)
59
  if not classes:
60
  st.info("No classes yet. Create one in Classroom Management.")
61
  return
@@ -65,14 +124,18 @@ def show_page():
65
  "Choose a class",
66
  list(range(len(classes))),
67
  index=0,
68
- format_func=lambda i: f"{classes[i]['name']}"
69
  )
70
  selected = classes[idx]
71
- class_id = selected["class_id"]
72
- code_row = dbapi.get_class(class_id)
 
 
 
 
73
 
74
  # get students before drawing chips
75
- rows = dbapi.class_student_metrics(class_id)
76
 
77
  # code + student chip row
78
  chip1, chip2 = st.columns([1, 1])
@@ -86,7 +149,7 @@ def show_page():
86
  f'<div class="sm-chip">👥 {len(rows)} Students</div>',
87
  unsafe_allow_html=True
88
  )
89
-
90
  st.markdown("---")
91
 
92
  # search line
@@ -96,19 +159,23 @@ def show_page():
96
  ).strip().lower()
97
 
98
  if query:
99
- rows = [r for r in rows if query in r["name"].lower() or query in r["email"].lower()]
 
 
 
100
 
101
  # student rows
102
  for r in rows:
103
- name = r["name"]
104
- email = r["email"]
105
- joined = str(r["joined_at"])[:10]
106
- total_xp = int(r["total_xp"] or 0)
107
  level = _level_from_xp(total_xp)
108
- lessons_completed = int(r["lessons_completed"] or 0)
109
- total_assigned = int(r["total_assigned_lessons"] or 0)
110
- avg_pct = round((r["avg_score"] or 0) * 100)
111
- streak = int(r["streak_days"] or 0)
 
112
 
113
  with st.container():
114
  st.markdown('<div class="sm-row">', unsafe_allow_html=True)
@@ -140,12 +207,17 @@ def show_page():
140
  d1, d2, spacer = st.columns([2, 1.3, 5])
141
  with d1:
142
  with st.popover("👁️ View Details"):
143
- # list the student's assignments
144
- items = dbapi.list_assignments_for_student(r["student_id"])
 
 
145
  if items:
146
  for it in items[:25]:
147
- tag = " + Quiz" if it["quiz_id"] else ""
148
- st.markdown(f"- **{it['title']}** · {it['subject']} · {it['level']}{tag} · Status: {it['status']}")
 
 
 
149
  else:
150
  st.info("No assignments yet.")
151
  with d2:
@@ -153,9 +225,9 @@ def show_page():
153
  st.download_button(
154
  "⬇️ Export",
155
  data=rep,
156
- file_name=f"{name.replace(' ','_')}_report.txt",
157
  mime="text/plain",
158
- key=f"dl_{r['student_id']}"
159
  )
160
 
161
- st.markdown('</div>', unsafe_allow_html=True)
 
1
  # phase/Teacher_view/studentlist.py
2
+ import os
3
  import streamlit as st
4
  from utils import db as dbapi
5
+ import utils.api as api # backend Space client
6
+
7
+ # Use local DB only when DISABLE_DB != "1"
8
+ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
9
 
10
  # ---------- tiny helpers ----------
11
  def _avatar(name: str) -> str:
 
12
  return "🧑‍🎓" if hash(name) % 2 else "👩‍🎓"
13
 
14
+ def _avg_pct_from_row(r) -> int:
15
+ """
16
+ Accepts either:
17
+ - r['avg_pct'] in [0, 100]
18
+ - r['avg_score'] in [0, 1] or [0, 100]
19
+ Returns an int 0..100.
20
+ """
21
+ v = r.get("avg_pct", r.get("avg_score", 0)) or 0
22
+ try:
23
+ f = float(v)
24
+ if f <= 1.0: # treat as 0..1
25
+ f *= 100.0
26
+ return max(0, min(100, int(round(f))))
27
+ except Exception:
28
+ return 0
29
+
30
+ def _level_from_xp(total_xp: int) -> int:
31
+ try:
32
+ xp = int(total_xp or 0)
33
+ except Exception:
34
+ xp = 0
35
+ return 1 + xp // 500
36
+
37
  def _report_text(r, level, avg_pct):
38
  return (
39
  "STUDENT PROGRESS REPORT\n"
40
  "======================\n"
41
+ f"Student: {r.get('name','')}\n"
42
+ f"Email: {r.get('email','')}\n"
43
+ f"Joined: {str(r.get('joined_at',''))[:10]}\n\n"
44
  "PROGRESS OVERVIEW\n"
45
  "-----------------\n"
46
+ f"Lessons Completed: {int(r.get('lessons_completed') or 0)}/{int(r.get('total_assigned_lessons') or 0)}\n"
47
  f"Average Quiz Score: {avg_pct}%\n"
48
+ f"Total XP: {int(r.get('total_xp') or 0)}\n"
49
  f"Current Level: {level}\n"
50
+ f"Study Streak: {int(r.get('streak_days') or 0)} days\n"
51
  )
52
 
 
 
 
 
 
 
 
 
53
  ROW_CSS = """
54
  <style>
55
  .sm-chip{display:inline-block;padding:4px 10px;border-radius:999px;background:#eef7f1;color:#0b8f5d;font-weight:600;font-size:.80rem;margin-left:8px}
 
65
  </style>
66
  """
67
 
68
+ # ---------- data access (DB or Backend) ----------
69
+ @st.cache_data(show_spinner=False, ttl=30)
70
+ def _list_classes_by_teacher(teacher_id: int):
71
+ if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"):
72
+ return dbapi.list_classes_by_teacher(teacher_id) or []
73
+ try:
74
+ return api.list_classes_by_teacher(teacher_id) or []
75
+ except Exception:
76
+ return []
77
+
78
+ @st.cache_data(show_spinner=False, ttl=30)
79
+ def _get_class(class_id: int):
80
+ if USE_LOCAL_DB and hasattr(dbapi, "get_class"):
81
+ return dbapi.get_class(class_id) or {}
82
+ try:
83
+ return api.get_class(class_id) or {}
84
+ except Exception:
85
+ return {}
86
+
87
+ @st.cache_data(show_spinner=False, ttl=30)
88
+ def _class_student_metrics(class_id: int):
89
+ if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"):
90
+ return dbapi.class_student_metrics(class_id) or []
91
+ try:
92
+ return api.class_student_metrics(class_id) or []
93
+ except Exception:
94
+ return []
95
+
96
+ @st.cache_data(show_spinner=False, ttl=30)
97
+ def _list_assignments_for_student(student_id: int):
98
+ if USE_LOCAL_DB and hasattr(dbapi, "list_assignments_for_student"):
99
+ return dbapi.list_assignments_for_student(student_id) or []
100
+ try:
101
+ return api.list_assignments_for_student(student_id) or []
102
+ except Exception:
103
+ return []
104
+
105
  # ---------- page ----------
106
  def show_page():
107
  st.title("🎓 Student Management")
108
  st.caption("Monitor and manage your students' progress")
109
  st.markdown(ROW_CSS, unsafe_allow_html=True)
110
 
111
+ teacher = st.session_state.get("user")
112
+ if not teacher:
113
+ st.error("Please log in.")
114
+ return
115
  teacher_id = teacher["user_id"]
116
 
117
+ classes = _list_classes_by_teacher(teacher_id)
118
  if not classes:
119
  st.info("No classes yet. Create one in Classroom Management.")
120
  return
 
124
  "Choose a class",
125
  list(range(len(classes))),
126
  index=0,
127
+ format_func=lambda i: f"{classes[i].get('name','(unnamed)')}"
128
  )
129
  selected = classes[idx]
130
+ class_id = selected.get("class_id") or selected.get("id") # be tolerant to backend naming
131
+ if class_id is None:
132
+ st.error("Selected class is missing an ID.")
133
+ return
134
+
135
+ code_row = _get_class(class_id)
136
 
137
  # get students before drawing chips
138
+ rows = _class_student_metrics(class_id)
139
 
140
  # code + student chip row
141
  chip1, chip2 = st.columns([1, 1])
 
149
  f'<div class="sm-chip">👥 {len(rows)} Students</div>',
150
  unsafe_allow_html=True
151
  )
152
+
153
  st.markdown("---")
154
 
155
  # search line
 
159
  ).strip().lower()
160
 
161
  if query:
162
+ rows = [
163
+ r for r in rows
164
+ if query in (r.get("name","").lower()) or query in (r.get("email","").lower())
165
+ ]
166
 
167
  # student rows
168
  for r in rows:
169
+ name = r.get("name", "Unknown")
170
+ email = r.get("email", "")
171
+ joined = str(r.get("joined_at", ""))[:10]
172
+ total_xp = int(r.get("total_xp") or 0)
173
  level = _level_from_xp(total_xp)
174
+ lessons_completed = int(r.get("lessons_completed") or 0)
175
+ total_assigned = int(r.get("total_assigned_lessons") or 0)
176
+ avg_pct = _avg_pct_from_row(r)
177
+ streak = int(r.get("streak_days") or 0)
178
+ student_id = r.get("student_id") or r.get("id")
179
 
180
  with st.container():
181
  st.markdown('<div class="sm-row">', unsafe_allow_html=True)
 
207
  d1, d2, spacer = st.columns([2, 1.3, 5])
208
  with d1:
209
  with st.popover("👁️ View Details"):
210
+ if student_id is not None:
211
+ items = _list_assignments_for_student(int(student_id))
212
+ else:
213
+ items = []
214
  if items:
215
  for it in items[:25]:
216
+ tag = " + Quiz" if it.get("quiz_id") else ""
217
+ st.markdown(
218
+ f"- **{it.get('title','Untitled')}** · {it.get('subject','General')} · "
219
+ f"{it.get('level','')} {tag} · Status: {it.get('status','unknown')}"
220
+ )
221
  else:
222
  st.info("No assignments yet.")
223
  with d2:
 
225
  st.download_button(
226
  "⬇️ Export",
227
  data=rep,
228
+ file_name=f"{str(name).replace(' ','_')}_report.txt",
229
  mime="text/plain",
230
+ key=f"dl_{student_id or name}"
231
  )
232
 
233
+ st.markdown('</div>', unsafe_allow_html=True)
utils/api.py CHANGED
@@ -87,6 +87,8 @@ def health():
87
  return {"ok": False}
88
 
89
  #---helpers
 
 
90
  def user_stats(student_id: int):
91
  return _req("GET", f"/students/{student_id}/stats").json()
92
  def list_assignments_for_student(student_id: int):
@@ -96,6 +98,167 @@ def student_quiz_average(student_id: int):
96
  def recent_lessons_for_student(student_id: int, limit: int = 5):
97
  return _req("GET", f"/students/{student_id}/recent", params={"limit": limit}).json()
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  # ---- Legacy agent endpoints (keep) ----
101
  def start_agent(student_id: int, lesson_id: int, level_slug: str):
 
87
  return {"ok": False}
88
 
89
  #---helpers
90
+
91
+ #--helpers for student_db.py
92
  def user_stats(student_id: int):
93
  return _req("GET", f"/students/{student_id}/stats").json()
94
  def list_assignments_for_student(student_id: int):
 
98
  def recent_lessons_for_student(student_id: int, limit: int = 5):
99
  return _req("GET", f"/students/{student_id}/recent", params={"limit": limit}).json()
100
 
101
+ # --- Teacher endpoints (backend Space) ---
102
+ def create_class(teacher_id: int, name: str):
103
+ return _json_or_raise(_req("POST", f"/teachers/{teacher_id}/classes",
104
+ json={"name": name}))
105
+
106
+ def teacher_tiles(teacher_id: int):
107
+ return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/tiles"))
108
+
109
+ def list_classes_by_teacher(teacher_id: int):
110
+ return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/classes"))
111
+
112
+ def class_student_metrics(class_id: int):
113
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/student_metrics"))
114
+
115
+ def class_weekly_activity(class_id: int):
116
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/weekly_activity"))
117
+
118
+ def class_progress_overview(class_id: int):
119
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/progress_overview"))
120
+
121
+ def class_recent_activity(class_id: int, limit=6, days=30):
122
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/recent_activity",
123
+ params={"limit": limit, "days": days}))
124
+
125
+ def list_students_in_class(class_id: int):
126
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/students"))
127
+
128
+ # Optional if you want to compute levels server-side
129
+ def level_from_xp(xp: int):
130
+ return _json_or_raise(_req("GET", "/levels/from_xp", params={"xp": xp}))["level"]
131
+
132
+ #--teacherlink.py helpers
133
+ def join_class_by_code(student_id: int, code: str):
134
+ d = _json_or_raise(_req("POST", f"/students/{student_id}/classes/join", json={"code": code}))
135
+ # backend may return {"class_id": ...} or full class object; both are fine
136
+ return d.get("class_id", d)
137
+
138
+ def list_classes_for_student(student_id: int):
139
+ return _json_or_raise(_req("GET", f"/students/{student_id}/classes"))
140
+
141
+ def class_content_counts(class_id: int):
142
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/counts"))
143
+
144
+ def student_class_progress(student_id: int, class_id: int):
145
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/progress"))
146
+
147
+ def leave_class(student_id: int, class_id: int):
148
+ # could also be DELETE /classes/{class_id}/students/{student_id}
149
+ _json_or_raise(_req("POST", f"/classes/{class_id}/leave", json={"student_id": student_id}))
150
+ return True
151
+
152
+ def student_assignments_for_class(student_id: int, class_id: int):
153
+ return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/assignments"))
154
+
155
+
156
+ # ---- Classes / Teacher endpoints ----
157
+ def create_class(teacher_id: int, name: str):
158
+ return _req("POST", f"/teachers/{teacher_id}/classes", json={"name": name}).json()
159
+
160
+ def list_classes_by_teacher(teacher_id: int):
161
+ return _req("GET", f"/teachers/{teacher_id}/classes").json()
162
+
163
+ def list_students_in_class(class_id: int):
164
+ return _req("GET", f"/classes/{class_id}/students").json()
165
+
166
+ def class_content_counts(class_id: int):
167
+ return _req("GET", f"/classes/{class_id}/content_counts").json()
168
+
169
+ def list_class_assignments(class_id: int):
170
+ return _req("GET", f"/classes/{class_id}/assignments").json()
171
+
172
+ def class_analytics(class_id: int):
173
+ return _req("GET", f"/classes/{class_id}/analytics").json()
174
+
175
+ #--contentmanage.py helpers
176
+
177
+ # ---------- Teacher/content management endpoints (backend Space) ----------
178
+ def list_classes_by_teacher(teacher_id: int):
179
+ return _req("GET", f"/teachers/{teacher_id}/classes").json()
180
+
181
+ def list_all_students_for_teacher(teacher_id: int):
182
+ return _req("GET", f"/teachers/{teacher_id}/students").json()
183
+
184
+ def list_lessons_by_teacher(teacher_id: int):
185
+ return _req("GET", f"/teachers/{teacher_id}/lessons").json()
186
+
187
+ def list_quizzes_by_teacher(teacher_id: int):
188
+ return _req("GET", f"/teachers/{teacher_id}/quizzes").json()
189
+
190
+ def create_lesson(teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
191
+ d = _req("POST", "/lessons", json={
192
+ "teacher_id": teacher_id, "title": title, "description": description,
193
+ "subject": subject, "level": level, "sections": sections
194
+ }).json()
195
+ return d["lesson_id"]
196
+
197
+ def update_lesson(lesson_id: int, teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
198
+ d = _req("PUT", f"/lessons/{lesson_id}", json={
199
+ "teacher_id": teacher_id, "title": title, "description": description,
200
+ "subject": subject, "level": level, "sections": sections
201
+ }).json()
202
+ return bool(d.get("ok", True))
203
+
204
+ def delete_lesson(lesson_id: int, teacher_id: int):
205
+ d = _req("DELETE", f"/lessons/{lesson_id}", json={"teacher_id": teacher_id}).json()
206
+ return bool(d.get("ok", True)), d.get("message", "")
207
+
208
+ def get_lesson(lesson_id: int):
209
+ return _req("GET", f"/lessons/{lesson_id}").json() # {"lesson":{...}, "sections":[...]}
210
+
211
+ def create_quiz(lesson_id: int, title: str, items: list[dict], settings: dict):
212
+ d = _req("POST", "/quizzes", json={"lesson_id": lesson_id, "title": title, "items": items, "settings": settings}).json()
213
+ return d["quiz_id"]
214
+
215
+ def update_quiz(quiz_id: int, teacher_id: int, title: str, items: list[dict], settings: dict):
216
+ d = _req("PUT", f"/quizzes/{quiz_id}", json={"teacher_id": teacher_id, "title": title, "items": items, "settings": settings}).json()
217
+ return bool(d.get("ok", True))
218
+
219
+ def delete_quiz(quiz_id: int, teacher_id: int):
220
+ d = _req("DELETE", f"/quizzes/{quiz_id}", json={"teacher_id": teacher_id}).json()
221
+ return bool(d.get("ok", True)), d.get("message", "")
222
+
223
+ def list_assigned_students_for_lesson(lesson_id: int):
224
+ return _req("GET", f"/lessons/{lesson_id}/assignees").json()
225
+
226
+ def list_assigned_students_for_quiz(quiz_id: int):
227
+ return _req("GET", f"/quizzes/{quiz_id}/assignees").json()
228
+
229
+ def assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, teacher_id: int):
230
+ d = _req("POST", "/assignments", json={
231
+ "lesson_id": lesson_id, "quiz_id": quiz_id, "class_id": class_id, "teacher_id": teacher_id
232
+ }).json()
233
+ return bool(d.get("ok", True))
234
+
235
+ # ---------- LLM-based quiz generation (backend uses GEN_MODEL) ----------
236
+ def generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "finance", level: str = "beginner"):
237
+ """
238
+ Backend should read GEN_MODEL from env and use your chosen model (llama-3.1-8b-instruct).
239
+ Return shape: {"items":[{"question":...,"options":[...],"answer_key":"A"}]}
240
+ """
241
+ return _req("POST", "/quiz/generate", json={
242
+ "content": content, "n_questions": n_questions, "subject": subject, "level": level
243
+ }).json()
244
+
245
+ #-- studentlist helpers
246
+
247
+ def list_classes_by_teacher(teacher_id: int):
248
+ return _req("GET", f"/teachers/{teacher_id}/classes").json()
249
+
250
+ def get_class(class_id: int):
251
+ return _req("GET", f"/classes/{class_id}").json()
252
+
253
+ def class_student_metrics(class_id: int):
254
+ # expected to return list of rows with fields used in the UI
255
+ return _req("GET", f"/classes/{class_id}/students").json()
256
+
257
+ def list_assignments_for_student(student_id: int):
258
+ return _req("GET", f"/students/{student_id}/assignments").json()
259
+
260
+
261
+
262
 
263
  # ---- Legacy agent endpoints (keep) ----
264
  def start_agent(student_id: int, lesson_id: int, level_slug: str):