ngwakomadikwe commited on
Commit
89144ca
Β·
verified Β·
1 Parent(s): 55dea68

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -109
app.py CHANGED
@@ -1,13 +1,12 @@
1
  """
2
- ThutoAI - Complete School Assistant with Dark Mode & Profile Pictures
3
  Meaning: "Thuto" = Learning/Education (Setswana β€” used for branding only)
4
 
5
- βœ… Student Accounts + Profile Pictures
6
- βœ… πŸŒ™ Real Dark Mode Toggle (persists per session)
7
- βœ… πŸŽ™οΈ Voice Input
8
- βœ… πŸ“… Assignment Tracker
9
- βœ… πŸ‘₯ Class Groups
10
- βœ… Modern UI with animations
11
  βœ… Fully commented
12
  """
13
 
@@ -18,8 +17,9 @@ from typing import List, Dict, Optional
18
  import time
19
  import json
20
  import base64
 
21
 
22
- # ==================== STUDENT SERVICE (Enhanced with Profile Pics) ====================
23
 
24
  class StudentService:
25
  """Manages student accounts, chat history, files, assignments, groups, and profile pictures."""
@@ -27,15 +27,16 @@ class StudentService:
27
  def __init__(self):
28
  self.students = {
29
  "student1": {"password": "pass123", "name": "John Doe", "avatar": None},
30
- "student2": {"password": "pass456", "name": "Jane Smith", "avatar": None}
 
31
  }
32
  self.student_sessions = {
33
  "student1": {
34
  "chat_history": [],
35
  "files": [],
36
  "assignments": [
37
- {"title": "Math Quiz", "due_date": "2025-04-25", "course": "MATH10A", "status": "pending"},
38
- {"title": "Science Lab Report", "due_date": "2025-04-30", "course": "SCI11B", "status": "pending"}
39
  ],
40
  "groups": ["MATH10A", "SCI11B"],
41
  "dark_mode": False
@@ -44,14 +45,22 @@ class StudentService:
44
  "chat_history": [],
45
  "files": [],
46
  "assignments": [
47
- {"title": "History Essay", "due_date": "2025-04-22", "course": "HIST9A", "status": "overdue"},
48
- {"title": "English Reading", "due_date": "2025-04-28", "course": "ENG10A", "status": "pending"}
49
  ],
50
  "groups": ["HIST9A", "ENG10A"],
51
  "dark_mode": True
 
 
 
 
 
 
 
52
  }
53
  }
54
  self.valid_groups = ["MATH10A", "MATH10B", "SCI11A", "SCI11B", "ENG10A", "ENG10B", "HIST9A", "HIST9B"]
 
55
 
56
  def register_student(self, username: str, password: str, name: str) -> str:
57
  if not username or not password or not name:
@@ -92,12 +101,8 @@ class StudentService:
92
 
93
  def update_avatar(self, username: str, avatar_path: str):
94
  if username in self.students and avatar_path:
95
- # In real app, save file and store path
96
- # Here we'll just store filename for demo
97
  self.students[username]["avatar"] = avatar_path
98
 
99
- # ... (all previous methods: get_chat_history, add_to_chat_history, etc. remain unchanged)
100
-
101
  def get_chat_history(self, username: str) -> List:
102
  return self.student_sessions.get(username, {}).get("chat_history", [])
103
 
@@ -116,8 +121,23 @@ class StudentService:
116
  })
117
 
118
  def get_assignments(self, username: str) -> List:
119
- assignments = self.student_sessions.get(username, {}).get("assignments", [])
120
- return sorted(assignments, key=lambda x: x["due_date"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  def get_groups(self, username: str) -> List:
123
  return self.student_sessions.get(username, {}).get("groups", [])
@@ -138,12 +158,54 @@ class StudentService:
138
  return f"βœ… Left group: {group_code.upper()}"
139
  return "❌ Group not found or not joined."
140
 
141
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  student_service = StudentService()
143
 
144
  # ==================== SCHOOL SERVICE ====================
145
 
146
  class SchoolService:
 
 
147
  def __init__(self):
148
  self.announcements = [
149
  {
@@ -153,7 +215,8 @@ class SchoolService:
153
  "course": "Mathematics",
154
  "date": "2025-04-15",
155
  "priority": "high",
156
- "posted_by": "Mr. Smith"
 
157
  },
158
  {
159
  "id": 2,
@@ -162,7 +225,8 @@ class SchoolService:
162
  "course": "Science",
163
  "date": "2025-04-12",
164
  "priority": "normal",
165
- "posted_by": "Dr. Lee"
 
166
  },
167
  {
168
  "id": 3,
@@ -171,12 +235,14 @@ class SchoolService:
171
  "course": "General",
172
  "date": "2025-04-10",
173
  "priority": "low",
174
- "posted_by": "Librarian"
 
175
  },
176
  ]
177
  self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
178
  self.total_announcements = len(self.announcements)
179
  self.total_files = 0
 
180
 
181
  def get_announcements(self, course_filter: str = "All") -> List[Dict]:
182
  if course_filter == "All":
@@ -192,7 +258,8 @@ class SchoolService:
192
  "course": course,
193
  "date": datetime.now().strftime("%Y-%m-%d"),
194
  "priority": priority,
195
- "posted_by": posted_by
 
196
  })
197
  self.total_announcements += 1
198
 
@@ -207,23 +274,53 @@ class SchoolService:
207
  "total_announcements": self.total_announcements,
208
  "total_files": self.total_files,
209
  "active_courses": len(set(a["course"] for a in self.announcements if a["course"] != "General")),
210
- "high_priority": len([a for a in self.announcements if a["priority"] == "high"])
 
211
  }
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  school_service = SchoolService()
215
 
216
  # ==================== ADMIN SERVICE ====================
217
 
218
  class AdminService:
 
 
219
  def __init__(self):
220
- self.admins = {
221
- "teacher@thutoai.edu": "password123",
222
- "admin@school.org": "letmein"
 
223
  }
224
 
225
- def authenticate(self, username: str, password: str) -> bool:
226
- return self.admins.get(username) == password
 
 
 
227
 
228
 
229
  admin_service = AdminService()
@@ -246,6 +343,7 @@ else:
246
  # ==================== AI CHAT FUNCTION ====================
247
 
248
  def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
 
249
  if not message.strip():
250
  return history, ""
251
 
@@ -305,6 +403,7 @@ Guidelines:
305
  # ==================== UI RENDERING HELPERS ====================
306
 
307
  def render_announcements(course: str) -> str:
 
308
  announcements = school_service.get_announcements(course)
309
  if not announcements:
310
  return """
@@ -350,6 +449,7 @@ def render_announcements(course: str) -> str:
350
  <span>πŸ“š {ann['course']}</span>
351
  <span>πŸ“… {ann['date']}</span>
352
  <span>πŸ‘¨β€πŸ« {ann['posted_by']}</span>
 
353
  </div>
354
  </div>
355
  </div>
@@ -360,6 +460,7 @@ def render_announcements(course: str) -> str:
360
 
361
 
362
  def render_assignments(assignments: List[Dict]) -> str:
 
363
  if not assignments:
364
  return """
365
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
@@ -403,8 +504,9 @@ def render_assignments(assignments: List[Dict]) -> str:
403
  <div style='display: flex; justify-content: space-between; align-items: flex-start;'>
404
  <div>
405
  <h3 style='margin: 0 0 8px 0; color: #212529;'>{task['title']}</h3>
406
- <div style='color: #6c757d; margin-bottom: 8px;'>πŸ“š {task['course']}</div>
407
  <div style='color: #495057;'>πŸ“… Due: {task['due_date']}</div>
 
408
  </div>
409
  <span style='
410
  background: {color};
@@ -424,6 +526,7 @@ def render_assignments(assignments: List[Dict]) -> str:
424
 
425
 
426
  def render_groups(groups: List[str]) -> str:
 
427
  if not groups:
428
  return """
429
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
@@ -468,9 +571,122 @@ def get_avatar_html(avatar_path: Optional[str], name: str) -> str:
468
  return img_html
469
 
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  # ==================== STATE MANAGEMENT ====================
472
 
473
  CURRENT_USER = "guest"
 
474
  DARK_MODE = False
475
 
476
  def login_student(username: str, password: str) -> tuple:
@@ -486,7 +702,6 @@ def login_student(username: str, password: str) -> tuple:
486
  avatar_html = get_avatar_html(student_data["avatar"], student_data["name"])
487
  welcome_msg = f"Welcome back, {student_data['name']}!"
488
 
489
- # Apply dark mode CSS if needed
490
  css_class = "dark-mode" if DARK_MODE else ""
491
 
492
  return (
@@ -554,8 +769,6 @@ def upload_avatar(file) -> str:
554
  return "❌ No file selected"
555
  if CURRENT_USER == "guest":
556
  return "❌ Please log in first"
557
- # In real app, save file to disk and store path
558
- # For demo, we'll just store the filename
559
  student_service.update_avatar(CURRENT_USER, file.name)
560
  return f"βœ… Avatar updated!"
561
 
@@ -581,6 +794,68 @@ def upload_file_for_student(file) -> str:
581
  student_service.add_file(CURRENT_USER, file.name)
582
  return result
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  # ==================== VOICE INPUT ====================
585
 
586
  VOICE_JS = """
@@ -612,50 +887,6 @@ async function startVoiceInput() {
612
  def voice_input_handler() -> str:
613
  return ""
614
 
615
- # ==================== TEACHER FUNCTIONS ====================
616
-
617
- IS_ADMIN = False
618
-
619
- def admin_login(username: str, password: str) -> tuple:
620
- global IS_ADMIN
621
- if admin_service.authenticate(username, password):
622
- IS_ADMIN = True
623
- stats = school_service.get_stats()
624
- stats_text = f"πŸ“Š Stats: {stats['total_announcements']} announcements, {stats['total_files']} files"
625
- return (
626
- gr.update(visible=False),
627
- gr.update(visible=True),
628
- stats_text,
629
- gr.update(visible=True)
630
- )
631
- return (
632
- gr.update(visible=True),
633
- gr.update(visible=False),
634
- "❌ Invalid credentials",
635
- gr.update(visible=False)
636
- )
637
-
638
- def admin_logout():
639
- global IS_ADMIN
640
- IS_ADMIN = False
641
- return (
642
- gr.update(visible=True),
643
- gr.update(visible=False),
644
- "",
645
- gr.update(visible=False)
646
- )
647
-
648
- def post_announcement(title: str, content: str, course: str, priority: str) -> str:
649
- if not IS_ADMIN:
650
- return "πŸ”’ Please log in first."
651
- if not title.strip():
652
- return "⚠️ Title is required."
653
- if not content.strip():
654
- return "⚠️ Content is required."
655
-
656
- school_service.add_announcement(title, content, course, priority)
657
- return f"βœ… Posted! πŸŽ‰ New announcement ID: {len(school_service.announcements)}"
658
-
659
  # ==================== CUSTOM CSS ====================
660
 
661
  CUSTOM_CSS = """
@@ -707,6 +938,13 @@ CUSTOM_CSS = """
707
  background: #2a2a2a !important;
708
  border-color: #5a5a5a !important;
709
  }
 
 
 
 
 
 
 
710
  """
711
 
712
  # ==================== BUILD UI ====================
@@ -762,8 +1000,7 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
762
  gr.Markdown("### πŸ’‘ Ask me anything β€” I'm here to help!")
763
  chatbot = gr.Chatbot(
764
  height=480,
765
- bubble_full_width=False,
766
- elem_classes=["chatbot-container"]
767
  )
768
  with gr.Row():
769
  msg = gr.Textbox(
@@ -853,7 +1090,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
853
  inputs=avatar_input,
854
  outputs=avatar_status
855
  )
856
- # Auto-refresh avatar on tab load
857
  demo.load(
858
  fn=lambda: get_avatar_html(
859
  student_service.students[CURRENT_USER]["avatar"] if CURRENT_USER != "guest" else None,
@@ -863,50 +1099,66 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
863
  outputs=avatar_display
864
  )
865
 
866
- with gr.Tab("πŸ” Teacher Admin"):
867
- gr.Markdown("### πŸ‘©β€πŸ« Post Announcements & View Stats")
868
 
869
  with gr.Group() as teacher_login_group:
870
- teacher_username = gr.Textbox(label="Username")
871
  teacher_password = gr.Textbox(label="Password", type="password")
872
  teacher_login_btn = gr.Button("πŸ”“ Login", variant="primary")
873
  teacher_status = gr.Textbox(label="Status")
874
 
875
  with gr.Group(visible=False) as teacher_dashboard:
876
- gr.Markdown("### πŸ“Š Dashboard Stats")
877
- stats = school_service.get_stats()
878
- gr.Markdown(f"""
879
- - πŸ“’ Total Announcements: **{stats['total_announcements']}**
880
- - πŸ“ Total Files Uploaded: **{stats['total_files']}**
881
- - 🎯 Active Courses: **{stats['active_courses']}**
882
- - 🚨 High Priority Posts: **{stats['high_priority']}**
883
- """)
884
-
885
- gr.Markdown("### ✍️ Create New Announcement")
886
- with gr.Row():
887
- ann_title = gr.Textbox(label="Title", placeholder="e.g., Quiz Moved to Friday", scale=3)
888
- ann_course = gr.Dropdown(choices=school_service.courses[1:], label="Course", value="General", scale=2)
889
- ann_content = gr.Textbox(label="Content", placeholder="Details for students...", lines=3)
890
- ann_priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal", inline=True)
891
- post_btn = gr.Button("πŸ“¬ Post Announcement", variant="primary")
892
- post_result = gr.Textbox(label="Result")
 
893
 
894
  teacher_logout_btn = gr.Button("⬅️ Logout", variant="secondary")
895
 
896
  teacher_login_btn.click(
897
- fn=admin_login,
898
  inputs=[teacher_username, teacher_password],
899
- outputs=[teacher_login_group, teacher_dashboard, teacher_status, post_btn]
900
  )
901
  teacher_logout_btn.click(
902
- fn=admin_logout,
903
  inputs=None,
904
- outputs=[teacher_login_group, teacher_dashboard, teacher_status, post_result]
 
 
 
 
 
905
  )
906
- post_btn.click(
907
- fn=post_announcement,
908
- inputs=[ann_title, ann_content, ann_course, ann_priority],
909
- outputs=post_result
 
 
 
 
 
 
 
 
 
 
910
  )
911
 
912
  # Logout button (already in header)
 
1
  """
2
+ ThutoAI - Complete School Assistant with Teacher Assignments & Analytics Dashboard
3
  Meaning: "Thuto" = Learning/Education (Setswana β€” used for branding only)
4
 
5
+ βœ… Teacher Assignment Posting β€” Assign work to class groups
6
+ βœ… πŸ“Š Analytics Dashboard β€” Track student engagement, assignments, groups
7
+ βœ… πŸŒ™ Dark Mode + πŸ–ΌοΈ Profile Pictures
8
+ βœ… πŸŽ™οΈ Voice Input + πŸ“… Assignment Tracker + πŸ‘₯ Class Groups
9
+ βœ… All deprecation warnings fixed (Gradio 4.0+ compatible)
 
10
  βœ… Fully commented
11
  """
12
 
 
17
  import time
18
  import json
19
  import base64
20
+ from collections import Counter
21
 
22
+ # ==================== STUDENT SERVICE ====================
23
 
24
  class StudentService:
25
  """Manages student accounts, chat history, files, assignments, groups, and profile pictures."""
 
27
  def __init__(self):
28
  self.students = {
29
  "student1": {"password": "pass123", "name": "John Doe", "avatar": None},
30
+ "student2": {"password": "pass456", "name": "Jane Smith", "avatar": None},
31
+ "student3": {"password": "pass789", "name": "Alex Johnson", "avatar": None}
32
  }
33
  self.student_sessions = {
34
  "student1": {
35
  "chat_history": [],
36
  "files": [],
37
  "assignments": [
38
+ {"id": 1, "title": "Math Quiz", "due_date": "2025-04-25", "course": "MATH10A", "status": "pending", "assigned_by": "Mr. Smith"},
39
+ {"id": 2, "title": "Science Lab Report", "due_date": "2025-04-30", "course": "SCI11B", "status": "pending", "assigned_by": "Dr. Lee"}
40
  ],
41
  "groups": ["MATH10A", "SCI11B"],
42
  "dark_mode": False
 
45
  "chat_history": [],
46
  "files": [],
47
  "assignments": [
48
+ {"id": 3, "title": "History Essay", "due_date": "2025-04-22", "course": "HIST9A", "status": "overdue", "assigned_by": "Ms. Brown"},
49
+ {"id": 4, "title": "English Reading", "due_date": "2025-04-28", "course": "ENG10A", "status": "pending", "assigned_by": "Mr. White"}
50
  ],
51
  "groups": ["HIST9A", "ENG10A"],
52
  "dark_mode": True
53
+ },
54
+ "student3": {
55
+ "chat_history": [],
56
+ "files": [],
57
+ "assignments": [],
58
+ "groups": ["MATH10A", "ENG10A"],
59
+ "dark_mode": False
60
  }
61
  }
62
  self.valid_groups = ["MATH10A", "MATH10B", "SCI11A", "SCI11B", "ENG10A", "ENG10B", "HIST9A", "HIST9B"]
63
+ self.all_assignments = [] # Master list for analytics
64
 
65
  def register_student(self, username: str, password: str, name: str) -> str:
66
  if not username or not password or not name:
 
101
 
102
  def update_avatar(self, username: str, avatar_path: str):
103
  if username in self.students and avatar_path:
 
 
104
  self.students[username]["avatar"] = avatar_path
105
 
 
 
106
  def get_chat_history(self, username: str) -> List:
107
  return self.student_sessions.get(username, {}).get("chat_history", [])
108
 
 
121
  })
122
 
123
  def get_assignments(self, username: str) -> List:
124
+ """Get assignments for groups student has joined."""
125
+ if username not in self.student_sessions:
126
+ return []
127
+ student_groups = set(self.student_sessions[username]["groups"])
128
+ all_assignments = []
129
+ for user_data in self.student_sessions.values():
130
+ for assignment in user_data.get("assignments", []):
131
+ if assignment["course"] in student_groups:
132
+ all_assignments.append(assignment)
133
+ # Remove duplicates by ID
134
+ seen = set()
135
+ unique_assignments = []
136
+ for assignment in all_assignments:
137
+ if assignment["id"] not in seen:
138
+ seen.add(assignment["id"])
139
+ unique_assignments.append(assignment)
140
+ return sorted(unique_assignments, key=lambda x: x["due_date"])
141
 
142
  def get_groups(self, username: str) -> List:
143
  return self.student_sessions.get(username, {}).get("groups", [])
 
158
  return f"βœ… Left group: {group_code.upper()}"
159
  return "❌ Group not found or not joined."
160
 
161
+ # Analytics methods
162
+ def get_total_students(self) -> int:
163
+ return len(self.students)
164
+
165
+ def get_active_students(self) -> int:
166
+ return len([s for s in self.student_sessions.keys() if len(self.student_sessions[s]["assignments"]) > 0])
167
+
168
+ def get_total_assignments(self) -> int:
169
+ total = 0
170
+ for user_data in self.student_sessions.values():
171
+ total += len(user_data.get("assignments", []))
172
+ return total
173
+
174
+ def get_completed_assignments(self) -> int:
175
+ completed = 0
176
+ for user_data in self.student_sessions.values():
177
+ for assignment in user_data.get("assignments", []):
178
+ if assignment["status"] == "completed":
179
+ completed += 1
180
+ return completed
181
+
182
+ def get_group_popularity(self) -> Dict:
183
+ all_groups = []
184
+ for user_data in self.student_sessions.values():
185
+ all_groups.extend(user_data.get("groups", []))
186
+ return dict(Counter(all_groups))
187
+
188
+ def get_recent_activity(self) -> List:
189
+ activity = []
190
+ for username, data in self.student_sessions.items():
191
+ if data["assignments"]:
192
+ latest = max(data["assignments"], key=lambda x: x.get("assigned_date", "2025-01-01"))
193
+ activity.append({
194
+ "student": self.students[username]["name"],
195
+ "last_assignment": latest["title"],
196
+ "date": latest.get("assigned_date", "Unknown")
197
+ })
198
+ return sorted(activity, key=lambda x: x["date"], reverse=True)[:5]
199
+
200
+
201
+ # Initialize student service
202
  student_service = StudentService()
203
 
204
  # ==================== SCHOOL SERVICE ====================
205
 
206
  class SchoolService:
207
+ """Handles announcements, AI context, and shared assignments."""
208
+
209
  def __init__(self):
210
  self.announcements = [
211
  {
 
215
  "course": "Mathematics",
216
  "date": "2025-04-15",
217
  "priority": "high",
218
+ "posted_by": "Mr. Smith",
219
+ "views": 45
220
  },
221
  {
222
  "id": 2,
 
225
  "course": "Science",
226
  "date": "2025-04-12",
227
  "priority": "normal",
228
+ "posted_by": "Dr. Lee",
229
+ "views": 32
230
  },
231
  {
232
  "id": 3,
 
235
  "course": "General",
236
  "date": "2025-04-10",
237
  "priority": "low",
238
+ "posted_by": "Librarian",
239
+ "views": 67
240
  },
241
  ]
242
  self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
243
  self.total_announcements = len(self.announcements)
244
  self.total_files = 0
245
+ self.assignment_id_counter = 5 # Start after existing assignments
246
 
247
  def get_announcements(self, course_filter: str = "All") -> List[Dict]:
248
  if course_filter == "All":
 
258
  "course": course,
259
  "date": datetime.now().strftime("%Y-%m-%d"),
260
  "priority": priority,
261
+ "posted_by": posted_by,
262
+ "views": 0
263
  })
264
  self.total_announcements += 1
265
 
 
274
  "total_announcements": self.total_announcements,
275
  "total_files": self.total_files,
276
  "active_courses": len(set(a["course"] for a in self.announcements if a["course"] != "General")),
277
+ "high_priority": len([a for a in self.announcements if a["priority"] == "high"]),
278
+ "total_views": sum(a["views"] for a in self.announcements)
279
  }
280
 
281
+ def create_assignment(self, title: str, description: str, due_date: str, course: str, assigned_by: str) -> int:
282
+ """Create assignment for a specific class group."""
283
+ assignment_id = self.assignment_id_counter
284
+ self.assignment_id_counter += 1
285
+
286
+ new_assignment = {
287
+ "id": assignment_id,
288
+ "title": title,
289
+ "description": description,
290
+ "due_date": due_date,
291
+ "course": course,
292
+ "assigned_by": assigned_by,
293
+ "assigned_date": datetime.now().strftime("%Y-%m-%d"),
294
+ "status": "pending"
295
+ }
296
+
297
+ # Assign to all students in this group
298
+ for username, data in student_service.student_sessions.items():
299
+ if course in data["groups"]:
300
+ data["assignments"].append(new_assignment.copy())
301
+
302
+ return assignment_id
303
+
304
 
305
  school_service = SchoolService()
306
 
307
  # ==================== ADMIN SERVICE ====================
308
 
309
  class AdminService:
310
+ """Handles teacher and admin authentication."""
311
+
312
  def __init__(self):
313
+ self.teachers = {
314
+ "teacher@thutoai.edu": {"password": "password123", "name": "Mr. Smith", "role": "teacher"},
315
+ "scienceteacher@school.org": {"password": "science123", "name": "Dr. Lee", "role": "teacher"},
316
+ "admin@school.org": {"password": "letmein", "name": "Admin", "role": "admin"}
317
  }
318
 
319
+ def authenticate(self, username: str, password: str) -> Optional[Dict]:
320
+ user = self.teachers.get(username)
321
+ if user and user["password"] == password:
322
+ return {"name": user["name"], "role": user["role"]}
323
+ return None
324
 
325
 
326
  admin_service = AdminService()
 
343
  # ==================== AI CHAT FUNCTION ====================
344
 
345
  def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
346
+ """Generate AI response with loading state and save to history."""
347
  if not message.strip():
348
  return history, ""
349
 
 
403
  # ==================== UI RENDERING HELPERS ====================
404
 
405
  def render_announcements(course: str) -> str:
406
+ """Render announcements with modern cards."""
407
  announcements = school_service.get_announcements(course)
408
  if not announcements:
409
  return """
 
449
  <span>πŸ“š {ann['course']}</span>
450
  <span>πŸ“… {ann['date']}</span>
451
  <span>πŸ‘¨β€πŸ« {ann['posted_by']}</span>
452
+ <span>πŸ‘οΈ {ann['views']} views</span>
453
  </div>
454
  </div>
455
  </div>
 
460
 
461
 
462
  def render_assignments(assignments: List[Dict]) -> str:
463
+ """Render assignments in a clean, prioritized list."""
464
  if not assignments:
465
  return """
466
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
 
504
  <div style='display: flex; justify-content: space-between; align-items: flex-start;'>
505
  <div>
506
  <h3 style='margin: 0 0 8px 0; color: #212529;'>{task['title']}</h3>
507
+ <div style='color: #6c757d; margin-bottom: 8px;'>πŸ“š {task['course']} Β· πŸ‘©β€πŸ« {task['assigned_by']}</div>
508
  <div style='color: #495057;'>πŸ“… Due: {task['due_date']}</div>
509
+ <div style='color: #6c757d; font-size: 0.9em;'>{task.get('description', '')}</div>
510
  </div>
511
  <span style='
512
  background: {color};
 
526
 
527
 
528
  def render_groups(groups: List[str]) -> str:
529
+ """Render joined class groups."""
530
  if not groups:
531
  return """
532
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
 
571
  return img_html
572
 
573
 
574
+ def render_analytics() -> str:
575
+ """Render analytics dashboard for admins."""
576
+ total_students = student_service.get_total_students()
577
+ active_students = student_service.get_active_students()
578
+ total_assignments = student_service.get_total_assignments()
579
+ completed_assignments = student_service.get_completed_assignments()
580
+ group_popularity = student_service.get_group_popularity()
581
+ recent_activity = student_service.get_recent_activity()
582
+ school_stats = school_service.get_stats()
583
+
584
+ completion_rate = (completed_assignments / total_assignments * 100) if total_assignments > 0 else 0
585
+
586
+ html = f"""
587
+ <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; margin-bottom: 32px;'>
588
+ <div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
589
+ <h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ‘₯ Student Engagement</h3>
590
+ <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
591
+ <div style='text-align: center; padding: 16px; background: #e3f2fd; border-radius: 8px;'>
592
+ <div style='font-size: 2em; color: #1976d2;'>{total_students}</div>
593
+ <div>Total Students</div>
594
+ </div>
595
+ <div style='text-align: center; padding: 16px; background: #e8f5e8; border-radius: 8px;'>
596
+ <div style='font-size: 2em; color: #2e7d32;'>{active_students}</div>
597
+ <div>Active Students</div>
598
+ </div>
599
+ </div>
600
+ </div>
601
+
602
+ <div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
603
+ <h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“Š Assignment Stats</h3>
604
+ <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
605
+ <div style='text-align: center; padding: 16px; background: #fff3e0; border-radius: 8px;'>
606
+ <div style='font-size: 2em; color: #ef6c00;'>{total_assignments}</div>
607
+ <div>Total Assignments</div>
608
+ </div>
609
+ <div style='text-align: center; padding: 16px; background: #f3e5f5; border-radius: 8px;'>
610
+ <div style='font-size: 2em; color: #7b1fa2;'>{completion_rate:.1f}%</div>
611
+ <div>Completion Rate</div>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </div>
616
+
617
+ <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 32px;'>
618
+ <div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
619
+ <h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“ˆ Popular Class Groups</h3>
620
+ <div style='display: grid; gap: 12px;'>
621
+ """
622
+
623
+ sorted_groups = sorted(group_popularity.items(), key=lambda x: x[1], reverse=True)
624
+ for group, count in sorted_groups[:5]:
625
+ percentage = (count / total_students * 100) if total_students > 0 else 0
626
+ html += f"""
627
+ <div style='display: flex; align-items: center; gap: 12px;'>
628
+ <div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
629
+ <div style='width: {percentage}%; background: #6e8efb; height: 100%; border-radius: 4px;'></div>
630
+ </div>
631
+ <div style='min-width: 80px;'>{group}</div>
632
+ <div style='color: #6c757d;'>{count} students</div>
633
+ </div>
634
+ """
635
+
636
+ html += """
637
+ </div>
638
+ </div>
639
+
640
+ <div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
641
+ <h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“’ Announcement Stats</h3>
642
+ <div style='display: grid; gap: 12px;'>
643
+ """
644
+
645
+ for ann in school_service.announcements[:5]:
646
+ views = ann.get("views", 0)
647
+ max_views = max(a.get("views", 0) for a in school_service.announcements) if school_service.announcements else 1
648
+ width_percent = (views / max_views * 100) if max_views > 0 else 0
649
+ html += f"""
650
+ <div style='display: flex; align-items: center; gap: 12px;'>
651
+ <div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
652
+ <div style='width: {width_percent}%; background: #ff7043; height: 100%; border-radius: 4px;'></div>
653
+ </div>
654
+ <div style='min-width: 120px; font-size: 0.9em;'>{ann['title'][:20]}...</div>
655
+ <div style='color: #6c757d;'>{views} views</div>
656
+ </div>
657
+ """
658
+
659
+ html += """
660
+ </div>
661
+ </div>
662
+ </div>
663
+
664
+ <div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
665
+ <h3 style='color: #212529; margin: 0 0 16px 0;'>⏱️ Recent Activity</h3>
666
+ <div style='display: grid; gap: 12px;'>
667
+ """
668
+
669
+ for activity in recent_activity:
670
+ html += f"""
671
+ <div style='padding: 12px; background: #f8f9fa; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;'>
672
+ <div>
673
+ <strong>{activity['student']}</strong> received assignment: <em>{activity['last_assignment']}</em>
674
+ </div>
675
+ <div style='color: #6c757d; font-size: 0.9em;'>{activity['date']}</div>
676
+ </div>
677
+ """
678
+
679
+ html += """
680
+ </div>
681
+ </div>
682
+ """
683
+
684
+ return html
685
+
686
  # ==================== STATE MANAGEMENT ====================
687
 
688
  CURRENT_USER = "guest"
689
+ CURRENT_TEACHER = None
690
  DARK_MODE = False
691
 
692
  def login_student(username: str, password: str) -> tuple:
 
702
  avatar_html = get_avatar_html(student_data["avatar"], student_data["name"])
703
  welcome_msg = f"Welcome back, {student_data['name']}!"
704
 
 
705
  css_class = "dark-mode" if DARK_MODE else ""
706
 
707
  return (
 
769
  return "❌ No file selected"
770
  if CURRENT_USER == "guest":
771
  return "❌ Please log in first"
 
 
772
  student_service.update_avatar(CURRENT_USER, file.name)
773
  return f"βœ… Avatar updated!"
774
 
 
794
  student_service.add_file(CURRENT_USER, file.name)
795
  return result
796
 
797
+ # ==================== TEACHER FUNCTIONS ====================
798
+
799
+ def teacher_login(username: str, password: str) -> tuple:
800
+ global CURRENT_TEACHER
801
+ teacher_data = admin_service.authenticate(username, password)
802
+ if teacher_data and teacher_data["role"] == "teacher":
803
+ CURRENT_TEACHER = teacher_data
804
+ return (
805
+ gr.update(visible=False),
806
+ gr.update(visible=True),
807
+ f"βœ… Welcome, {teacher_data['name']}!",
808
+ gr.update(visible=True)
809
+ )
810
+ elif teacher_data and teacher_data["role"] == "admin":
811
+ CURRENT_TEACHER = teacher_data
812
+ return (
813
+ gr.update(visible=False),
814
+ gr.update(visible=True),
815
+ f"βœ… Welcome, Admin {teacher_data['name']}!",
816
+ gr.update(visible=True)
817
+ )
818
+ return (
819
+ gr.update(visible=True),
820
+ gr.update(visible=False),
821
+ "❌ Invalid credentials or not a teacher account",
822
+ gr.update(visible=False)
823
+ )
824
+
825
+ def teacher_logout():
826
+ global CURRENT_TEACHER
827
+ CURRENT_TEACHER = None
828
+ return (
829
+ gr.update(visible=True),
830
+ gr.update(visible=False),
831
+ "",
832
+ gr.update(visible=False)
833
+ )
834
+
835
+ def create_assignment(title: str, description: str, due_date: str, course: str) -> str:
836
+ if not CURRENT_TEACHER:
837
+ return "πŸ”’ Please log in as teacher first."
838
+ if not title or not due_date or not course:
839
+ return "⚠️ Title, due date, and course are required."
840
+ if course not in student_service.valid_groups:
841
+ return "⚠️ Invalid course/group. Choose from available groups."
842
+
843
+ assignment_id = school_service.create_assignment(
844
+ title=title,
845
+ description=description,
846
+ due_date=due_date,
847
+ course=course,
848
+ assigned_by=CURRENT_TEACHER["name"]
849
+ )
850
+
851
+ # Count how many students received this assignment
852
+ recipients = 0
853
+ for data in student_service.student_sessions.values():
854
+ if course in data["groups"]:
855
+ recipients += 1
856
+
857
+ return f"βœ… Assignment created! ID: {assignment_id}. Sent to {recipients} students in {course}."
858
+
859
  # ==================== VOICE INPUT ====================
860
 
861
  VOICE_JS = """
 
887
  def voice_input_handler() -> str:
888
  return ""
889
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  # ==================== CUSTOM CSS ====================
891
 
892
  CUSTOM_CSS = """
 
938
  background: #2a2a2a !important;
939
  border-color: #5a5a5a !important;
940
  }
941
+
942
+ .analytics-card {
943
+ background: white;
944
+ padding: 20px;
945
+ border-radius: 12px;
946
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
947
+ }
948
  """
949
 
950
  # ==================== BUILD UI ====================
 
1000
  gr.Markdown("### πŸ’‘ Ask me anything β€” I'm here to help!")
1001
  chatbot = gr.Chatbot(
1002
  height=480,
1003
+ type='messages' # βœ… FIXED: Replaced deprecated bubble_full_width
 
1004
  )
1005
  with gr.Row():
1006
  msg = gr.Textbox(
 
1090
  inputs=avatar_input,
1091
  outputs=avatar_status
1092
  )
 
1093
  demo.load(
1094
  fn=lambda: get_avatar_html(
1095
  student_service.students[CURRENT_USER]["avatar"] if CURRENT_USER != "guest" else None,
 
1099
  outputs=avatar_display
1100
  )
1101
 
1102
+ with gr.Tab("πŸ‘©β€πŸ« Teacher Panel"):
1103
+ gr.Markdown("### πŸ”‘ Teacher Login (for assignment creation)")
1104
 
1105
  with gr.Group() as teacher_login_group:
1106
+ teacher_username = gr.Textbox(label="Teacher Username")
1107
  teacher_password = gr.Textbox(label="Password", type="password")
1108
  teacher_login_btn = gr.Button("πŸ”“ Login", variant="primary")
1109
  teacher_status = gr.Textbox(label="Status")
1110
 
1111
  with gr.Group(visible=False) as teacher_dashboard:
1112
+ gr.Markdown("### ✍️ Create New Assignment")
1113
+ assignment_title = gr.Textbox(label="Assignment Title", placeholder="e.g., Chapter 5 Quiz")
1114
+ assignment_description = gr.Textbox(label="Description", placeholder="Instructions for students...", lines=2)
1115
+ assignment_due_date = gr.Textbox(label="Due Date (YYYY-MM-DD)", placeholder="2025-05-01")
1116
+ assignment_course = gr.Dropdown(
1117
+ choices=student_service.valid_groups,
1118
+ label="Assign to Class Group",
1119
+ value="MATH10A"
1120
+ )
1121
+ create_assignment_btn = gr.Button("πŸ“¬ Create Assignment", variant="primary")
1122
+ assignment_result = gr.Textbox(label="Result")
1123
+
1124
+ # βœ… FIXED: Removed deprecated 'inline' parameter
1125
+ assignment_priority = gr.Radio(
1126
+ ["low", "normal", "high"],
1127
+ label="Priority",
1128
+ value="normal"
1129
+ )
1130
 
1131
  teacher_logout_btn = gr.Button("⬅️ Logout", variant="secondary")
1132
 
1133
  teacher_login_btn.click(
1134
+ fn=teacher_login,
1135
  inputs=[teacher_username, teacher_password],
1136
+ outputs=[teacher_login_group, teacher_dashboard, teacher_status, create_assignment_btn]
1137
  )
1138
  teacher_logout_btn.click(
1139
+ fn=teacher_logout,
1140
  inputs=None,
1141
+ outputs=[teacher_login_group, teacher_dashboard, teacher_status, assignment_result]
1142
+ )
1143
+ create_assignment_btn.click(
1144
+ fn=create_assignment,
1145
+ inputs=[assignment_title, assignment_description, assignment_due_date, assignment_course],
1146
+ outputs=assignment_result
1147
  )
1148
+
1149
+ with gr.Tab("πŸ“Š Admin Analytics"):
1150
+ gr.Markdown("### πŸ“ˆ School Analytics Dashboard")
1151
+ analytics_display = gr.HTML()
1152
+ refresh_analytics_btn = gr.Button("πŸ”„ Refresh Analytics")
1153
+ refresh_analytics_btn.click(
1154
+ fn=render_analytics,
1155
+ inputs=None,
1156
+ outputs=analytics_display
1157
+ )
1158
+ demo.load(
1159
+ fn=render_analytics,
1160
+ inputs=None,
1161
+ outputs=analytics_display
1162
  )
1163
 
1164
  # Logout button (already in header)