ngwakomadikwe commited on
Commit
7b56738
Β·
verified Β·
1 Parent(s): 725b720

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -88
app.py CHANGED
@@ -1,13 +1,14 @@
1
  """
2
- ThutoAI - Complete School Assistant with Voice, Assignments & Class Groups
3
  Meaning: "Thuto" = Learning/Education (Setswana β€” used for branding only)
4
 
5
- βœ… Student Accounts (saved chat history & files)
6
- βœ… πŸŽ™οΈ Voice Input (Web Speech API)
7
- βœ… πŸ“… Assignment & Deadline Tracker (Calendar UI)
8
- βœ… πŸ‘₯ Class Groups (Students join classes like "MATH10A")
9
- βœ… Modern UI with dark mode, animations, teacher dashboard
10
- βœ… Fully commented for easy understanding
 
11
  """
12
 
13
  import os
@@ -16,20 +17,18 @@ from datetime import datetime, timedelta
16
  from typing import List, Dict, Optional
17
  import time
18
  import json
19
- import random
20
 
21
- # ==================== STUDENT ACCOUNTS SERVICE ====================
22
 
23
  class StudentService:
24
- """Manages student accounts, chat history, files, assignments, and class groups."""
25
 
26
  def __init__(self):
27
- # In-memory storage β€” replace with SQLite in production
28
  self.students = {
29
- "student1": {"password": "pass123", "name": "John Doe"},
30
- "student2": {"password": "pass456", "name": "Jane Smith"}
31
  }
32
- # Structure: {username: {chat_history: [], files: [], assignments: [], groups: []}}
33
  self.student_sessions = {
34
  "student1": {
35
  "chat_history": [],
@@ -38,7 +37,8 @@ class StudentService:
38
  {"title": "Math Quiz", "due_date": "2025-04-25", "course": "MATH10A", "status": "pending"},
39
  {"title": "Science Lab Report", "due_date": "2025-04-30", "course": "SCI11B", "status": "pending"}
40
  ],
41
- "groups": ["MATH10A", "SCI11B"]
 
42
  },
43
  "student2": {
44
  "chat_history": [],
@@ -47,29 +47,28 @@ class StudentService:
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
  }
52
  }
53
- # Predefined valid class groups
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
- """Register new student."""
58
  if not username or not password or not name:
59
  return "⚠️ All fields required."
60
  if username in self.students:
61
  return "⚠️ Username already exists."
62
- self.students[username] = {"password": password, "name": name}
63
  self.student_sessions[username] = {
64
  "chat_history": [],
65
  "files": [],
66
  "assignments": [],
67
- "groups": []
 
68
  }
69
  return "βœ… Account created! Please log in."
70
 
71
- def authenticate_student(self, username: str, password: str) -> Optional[str]:
72
- """Authenticate student and return name if successful."""
73
  student = self.students.get(username)
74
  if student and student["password"] == password:
75
  if username not in self.student_sessions:
@@ -77,11 +76,28 @@ class StudentService:
77
  "chat_history": [],
78
  "files": [],
79
  "assignments": [],
80
- "groups": []
 
81
  }
82
- return student["name"]
 
 
 
 
83
  return None
84
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def get_chat_history(self, username: str) -> List:
86
  return self.student_sessions.get(username, {}).get("chat_history", [])
87
 
@@ -100,21 +116,9 @@ class StudentService:
100
  })
101
 
102
  def get_assignments(self, username: str) -> List:
103
- """Get student's assignments sorted by due date."""
104
  assignments = self.student_sessions.get(username, {}).get("assignments", [])
105
- # Sort by due date
106
  return sorted(assignments, key=lambda x: x["due_date"])
107
 
108
- def add_assignment(self, username: str, title: str, due_date: str, course: str):
109
- """Add assignment (used by teacher or self)."""
110
- if username in self.student_sessions:
111
- self.student_sessions[username]["assignments"].append({
112
- "title": title,
113
- "due_date": due_date,
114
- "course": course,
115
- "status": "pending"
116
- })
117
-
118
  def get_groups(self, username: str) -> List:
119
  return self.student_sessions.get(username, {}).get("groups", [])
120
 
@@ -135,14 +139,11 @@ class StudentService:
135
  return "❌ Group not found or not joined."
136
 
137
 
138
- # Initialize student service
139
  student_service = StudentService()
140
 
141
  # ==================== SCHOOL SERVICE ====================
142
 
143
  class SchoolService:
144
- """Handles announcements, AI context, and shared assignments."""
145
-
146
  def __init__(self):
147
  self.announcements = [
148
  {
@@ -215,8 +216,6 @@ school_service = SchoolService()
215
  # ==================== ADMIN SERVICE ====================
216
 
217
  class AdminService:
218
- """Handles teacher authentication."""
219
-
220
  def __init__(self):
221
  self.admins = {
222
  "[email protected]": "password123",
@@ -247,11 +246,9 @@ else:
247
  # ==================== AI CHAT FUNCTION ====================
248
 
249
  def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
250
- """Generate AI response with loading state and save to history."""
251
  if not message.strip():
252
  return history, ""
253
 
254
- # Show "thinking" state
255
  thinking_msg = "πŸ€” ThutoAI is thinking..."
256
  history.append((message, thinking_msg))
257
  yield history, ""
@@ -281,7 +278,6 @@ Guidelines:
281
 
282
  reply = response.choices[0].message.content.strip()
283
 
284
- # Append relevant announcements
285
  keywords = ["exam", "test", "due", "assignment", "deadline", "when", "what", "grade", "score"]
286
  if any(kw in message.lower() for kw in keywords):
287
  matches = school_service.get_announcements()
@@ -298,10 +294,8 @@ Guidelines:
298
  time.sleep(1.5)
299
  reply = f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\nπŸ’‘ *Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!*"
300
 
301
- # Replace thinking message with real reply
302
  history[-1] = (message, reply)
303
 
304
- # Save to student's history if logged in
305
  if username != "guest":
306
  student_service.add_to_chat_history(username, message, reply)
307
 
@@ -311,7 +305,6 @@ Guidelines:
311
  # ==================== UI RENDERING HELPERS ====================
312
 
313
  def render_announcements(course: str) -> str:
314
- """Render announcements with modern cards."""
315
  announcements = school_service.get_announcements(course)
316
  if not announcements:
317
  return """
@@ -367,7 +360,6 @@ def render_announcements(course: str) -> str:
367
 
368
 
369
  def render_assignments(assignments: List[Dict]) -> str:
370
- """Render assignments in a clean, prioritized list."""
371
  if not assignments:
372
  return """
373
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
@@ -432,7 +424,6 @@ def render_assignments(assignments: List[Dict]) -> str:
432
 
433
 
434
  def render_groups(groups: List[str]) -> str:
435
- """Render joined class groups."""
436
  if not groups:
437
  return """
438
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
@@ -462,31 +453,55 @@ def render_groups(groups: List[str]) -> str:
462
  return html
463
 
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  # ==================== STATE MANAGEMENT ====================
466
 
467
  CURRENT_USER = "guest"
 
468
 
469
  def login_student(username: str, password: str) -> tuple:
470
- """Login student and load their data."""
471
- global CURRENT_USER
472
- student_name = student_service.authenticate_student(username, password)
473
- if student_name:
474
  CURRENT_USER = username
 
475
  chat_history = student_service.get_chat_history(username)
476
  files = student_service.get_files(username)
477
  assignments = student_service.get_assignments(username)
478
  groups = student_service.get_groups(username)
479
- welcome_msg = f"Welcome back, {student_name}!"
 
 
 
 
 
480
  return (
481
- gr.update(visible=False), # Hide login
482
- gr.update(visible=True), # Show main UI
483
  chat_history,
484
  files,
485
  render_assignments(assignments),
486
  render_groups(groups),
487
  welcome_msg,
488
- gr.update(value=student_name, visible=True),
489
- gr.update(visible=True) # Show logout
 
 
 
490
  )
491
  return (
492
  gr.update(visible=True),
@@ -497,27 +512,53 @@ def login_student(username: str, password: str) -> tuple:
497
  "",
498
  "❌ Invalid username or password",
499
  gr.update(visible=False),
500
- gr.update(visible=False)
 
 
 
501
  )
502
 
503
  def register_student(username: str, password: str, name: str) -> str:
504
  return student_service.register_student(username, password, name)
505
 
506
  def logout_student() -> tuple:
507
- global CURRENT_USER
508
  CURRENT_USER = "guest"
 
509
  return (
510
- gr.update(visible=True), # Show login
511
- gr.update(visible=False), # Hide main UI
512
  [],
513
  [],
514
  "",
515
  "",
516
  "",
517
  gr.update(visible=False),
518
- gr.update(visible=False)
 
 
 
519
  )
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  def join_group(group_code: str) -> tuple:
522
  if CURRENT_USER == "guest":
523
  return "❌ Please log in first.", ""
@@ -540,7 +581,7 @@ def upload_file_for_student(file) -> str:
540
  student_service.add_file(CURRENT_USER, file.name)
541
  return result
542
 
543
- # ==================== VOICE INPUT (Web Speech API) ====================
544
 
545
  VOICE_JS = """
546
  async function startVoiceInput() {
@@ -569,7 +610,6 @@ async function startVoiceInput() {
569
  """
570
 
571
  def voice_input_handler() -> str:
572
- """Placeholder function β€” actual voice handled by JS."""
573
  return ""
574
 
575
  # ==================== TEACHER FUNCTIONS ====================
@@ -624,39 +664,58 @@ CUSTOM_CSS = """
624
  max-width: 1200px;
625
  margin: 0 auto;
626
  padding: 16px;
 
 
 
 
 
 
 
 
 
 
 
627
  }
 
628
  .primary {
629
  background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
630
  border: none !important;
631
  color: white !important;
632
  }
 
633
  .chatbot-container {
634
  background: #f8f9fa !important;
635
  border-radius: 16px !important;
636
  }
 
 
 
 
 
637
  .user, .bot {
638
  border-radius: 18px !important;
639
  padding: 12px 16px !important;
640
  }
 
641
  .file-upload {
642
  border: 2px dashed #6e8efb !important;
643
  border-radius: 12px !important;
644
  background: #f8f9ff !important;
645
  }
646
- .dark-mode {
647
- background: #1e1e1e !important;
648
- color: #f0f0f0 !important;
 
649
  }
650
  """
651
 
652
  # ==================== BUILD UI ====================
653
 
654
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
655
- # Header
656
- gr.Markdown("# πŸŽ“ ThutoAI β€” Your AI School Assistant")
657
-
658
- # Dark mode button
659
- dark_mode_btn = gr.Button("πŸŒ™ Toggle Dark Mode", variant="secondary")
660
 
661
  # ========= STUDENT LOGIN/REGISTER =========
662
  with gr.Group() as login_group:
@@ -668,7 +727,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
668
  register_btn = gr.Button("πŸ“ Register", variant="secondary")
669
  login_status = gr.Textbox(label="Status", interactive=False)
670
 
671
- # Register modal
672
  with gr.Accordion("πŸ“ Register New Account", open=False):
673
  reg_username = gr.Textbox(label="Username")
674
  reg_password = gr.Textbox(label="Password", type="password")
@@ -678,6 +736,12 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
678
 
679
  # ========= MAIN APP (hidden until login) =========
680
  with gr.Group(visible=False) as main_app:
 
 
 
 
 
 
681
  with gr.Tabs():
682
  with gr.Tab("πŸ“’ Announcements"):
683
  gr.Markdown("### Filter by Course or Subject")
@@ -719,7 +783,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
719
  submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
720
  clear_btn.click(lambda: [], None, chatbot)
721
 
722
- # Voice input (JS-based)
723
  voice_btn.click(
724
  fn=voice_input_handler,
725
  inputs=None,
@@ -779,6 +842,27 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
779
  outputs=file_list
780
  )
781
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
  with gr.Tab("πŸ” Teacher Admin"):
783
  gr.Markdown("### πŸ‘©β€πŸ« Post Announcements & View Stats")
784
 
@@ -825,18 +909,7 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
825
  outputs=post_result
826
  )
827
 
828
- # Logout button
829
- with gr.Row():
830
- user_display = gr.Textbox(label="Logged in as", interactive=False, visible=False)
831
- logout_btn = gr.Button("⬅️ Logout", variant="secondary", visible=False)
832
- logout_btn.click(
833
- fn=logout_student,
834
- inputs=None,
835
- outputs=[
836
- login_group, main_app, chatbot, gr.update(), gr.update(), gr.update(),
837
- login_status, user_display, logout_btn
838
- ]
839
- )
840
 
841
  # ========= EVENT HANDLERS =========
842
  login_btn.click(
@@ -844,7 +917,7 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
844
  inputs=[login_username, login_password],
845
  outputs=[
846
  login_group, main_app, chatbot, gr.update(), assignments_display, groups_display,
847
- login_status, user_display, logout_btn
848
  ]
849
  )
850
 
@@ -854,6 +927,12 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
854
  outputs=reg_status
855
  )
856
 
 
 
 
 
 
 
857
  # Launch app
858
  if __name__ == "__main__":
859
  demo.launch()
 
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
 
14
  import os
 
17
  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."""
26
 
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": [],
 
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
42
  },
43
  "student2": {
44
  "chat_history": [],
 
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:
58
  return "⚠️ All fields required."
59
  if username in self.students:
60
  return "⚠️ Username already exists."
61
+ self.students[username] = {"password": password, "name": name, "avatar": None}
62
  self.student_sessions[username] = {
63
  "chat_history": [],
64
  "files": [],
65
  "assignments": [],
66
+ "groups": [],
67
+ "dark_mode": False
68
  }
69
  return "βœ… Account created! Please log in."
70
 
71
+ def authenticate_student(self, username: str, password: str) -> Optional[Dict]:
 
72
  student = self.students.get(username)
73
  if student and student["password"] == password:
74
  if username not in self.student_sessions:
 
76
  "chat_history": [],
77
  "files": [],
78
  "assignments": [],
79
+ "groups": [],
80
+ "dark_mode": False
81
  }
82
+ return {
83
+ "name": student["name"],
84
+ "avatar": student["avatar"],
85
+ "dark_mode": self.student_sessions[username]["dark_mode"]
86
+ }
87
  return None
88
 
89
+ def update_dark_mode(self, username: str, is_dark: bool):
90
+ if username in self.student_sessions:
91
+ self.student_sessions[username]["dark_mode"] = is_dark
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
  })
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", [])
124
 
 
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
  {
 
216
  # ==================== ADMIN SERVICE ====================
217
 
218
  class AdminService:
 
 
219
  def __init__(self):
220
  self.admins = {
221
  "[email protected]": "password123",
 
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
 
 
252
  thinking_msg = "πŸ€” ThutoAI is thinking..."
253
  history.append((message, thinking_msg))
254
  yield history, ""
 
278
 
279
  reply = response.choices[0].message.content.strip()
280
 
 
281
  keywords = ["exam", "test", "due", "assignment", "deadline", "when", "what", "grade", "score"]
282
  if any(kw in message.lower() for kw in keywords):
283
  matches = school_service.get_announcements()
 
294
  time.sleep(1.5)
295
  reply = f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\nπŸ’‘ *Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!*"
296
 
 
297
  history[-1] = (message, reply)
298
 
 
299
  if username != "guest":
300
  student_service.add_to_chat_history(username, message, reply)
301
 
 
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 """
 
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;'>
 
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;'>
 
453
  return html
454
 
455
 
456
+ def get_avatar_html(avatar_path: Optional[str], name: str) -> str:
457
+ """Generate HTML for avatar display."""
458
+ if avatar_path:
459
+ try:
460
+ with open(avatar_path, "rb") as f:
461
+ img_data = base64.b64encode(f.read()).decode()
462
+ img_html = f'<img src="data:image/png;base64,{img_data}" style="width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 2px solid #6e8efb;">'
463
+ except:
464
+ img_html = f'<div style="width: 60px; height: 60px; border-radius: 50%; background: #6e8efb; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2em; border: 2px solid #6e8efb;">{name[0].upper()}</div>'
465
+ else:
466
+ img_html = f'<div style="width: 60px; height: 60px; border-radius: 50%; background: #6e8efb; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2em; border: 2px solid #6e8efb;">{name[0].upper()}</div>'
467
+
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:
477
+ global CURRENT_USER, DARK_MODE
478
+ student_data = student_service.authenticate_student(username, password)
479
+ if student_data:
 
480
  CURRENT_USER = username
481
+ DARK_MODE = student_data["dark_mode"]
482
  chat_history = student_service.get_chat_history(username)
483
  files = student_service.get_files(username)
484
  assignments = student_service.get_assignments(username)
485
  groups = student_service.get_groups(username)
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 (
493
+ gr.update(visible=False),
494
+ gr.update(visible=True),
495
  chat_history,
496
  files,
497
  render_assignments(assignments),
498
  render_groups(groups),
499
  welcome_msg,
500
+ gr.update(value=student_data["name"], visible=True),
501
+ gr.update(visible=True),
502
+ gr.update(value=avatar_html),
503
+ gr.update(value="πŸŒ™ Light Mode" if DARK_MODE else "β˜€οΈ Dark Mode"),
504
+ css_class
505
  )
506
  return (
507
  gr.update(visible=True),
 
512
  "",
513
  "❌ Invalid username or password",
514
  gr.update(visible=False),
515
+ gr.update(visible=False),
516
+ gr.update(),
517
+ gr.update(),
518
+ ""
519
  )
520
 
521
  def register_student(username: str, password: str, name: str) -> str:
522
  return student_service.register_student(username, password, name)
523
 
524
  def logout_student() -> tuple:
525
+ global CURRENT_USER, DARK_MODE
526
  CURRENT_USER = "guest"
527
+ DARK_MODE = False
528
  return (
529
+ gr.update(visible=True),
530
+ gr.update(visible=False),
531
  [],
532
  [],
533
  "",
534
  "",
535
  "",
536
  gr.update(visible=False),
537
+ gr.update(visible=False),
538
+ gr.update(),
539
+ gr.update(value="β˜€οΈ Dark Mode"),
540
+ ""
541
  )
542
 
543
+ def toggle_dark_mode() -> tuple:
544
+ global DARK_MODE, CURRENT_USER
545
+ DARK_MODE = not DARK_MODE
546
+ if CURRENT_USER != "guest":
547
+ student_service.update_dark_mode(CURRENT_USER, DARK_MODE)
548
+ btn_text = "πŸŒ™ Light Mode" if DARK_MODE else "β˜€οΈ Dark Mode"
549
+ css_class = "dark-mode" if DARK_MODE else ""
550
+ return btn_text, css_class
551
+
552
+ def upload_avatar(file) -> str:
553
+ if not file:
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
+
562
  def join_group(group_code: str) -> tuple:
563
  if CURRENT_USER == "guest":
564
  return "❌ Please log in first.", ""
 
581
  student_service.add_file(CURRENT_USER, file.name)
582
  return result
583
 
584
+ # ==================== VOICE INPUT ====================
585
 
586
  VOICE_JS = """
587
  async function startVoiceInput() {
 
610
  """
611
 
612
  def voice_input_handler() -> str:
 
613
  return ""
614
 
615
  # ==================== TEACHER FUNCTIONS ====================
 
664
  max-width: 1200px;
665
  margin: 0 auto;
666
  padding: 16px;
667
+ transition: background-color 0.3s, color 0.3s;
668
+ }
669
+
670
+ .dark-mode {
671
+ --background-fill-primary: #1e1e1e !important;
672
+ --background-fill-secondary: #2d2d2d !important;
673
+ --text-color: #f0f0f0 !important;
674
+ --button-primary-background-fill: #5a5a5a !important;
675
+ --button-secondary-background-fill: #3a3a3a !important;
676
+ --input-background-fill: #2d2d2d !important;
677
+ --input-border-color: #444 !important;
678
  }
679
+
680
  .primary {
681
  background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
682
  border: none !important;
683
  color: white !important;
684
  }
685
+
686
  .chatbot-container {
687
  background: #f8f9fa !important;
688
  border-radius: 16px !important;
689
  }
690
+
691
+ .dark-mode .chatbot-container {
692
+ background: #2d2d2d !important;
693
+ }
694
+
695
  .user, .bot {
696
  border-radius: 18px !important;
697
  padding: 12px 16px !important;
698
  }
699
+
700
  .file-upload {
701
  border: 2px dashed #6e8efb !important;
702
  border-radius: 12px !important;
703
  background: #f8f9ff !important;
704
  }
705
+
706
+ .dark-mode .file-upload {
707
+ background: #2a2a2a !important;
708
+ border-color: #5a5a5a !important;
709
  }
710
  """
711
 
712
  # ==================== BUILD UI ====================
713
 
714
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
715
+ # Header with dark mode toggle
716
+ with gr.Row():
717
+ gr.Markdown("# πŸŽ“ ThutoAI β€” Your AI School Assistant")
718
+ dark_mode_btn = gr.Button("β˜€οΈ Dark Mode", variant="secondary")
 
719
 
720
  # ========= STUDENT LOGIN/REGISTER =========
721
  with gr.Group() as login_group:
 
727
  register_btn = gr.Button("πŸ“ Register", variant="secondary")
728
  login_status = gr.Textbox(label="Status", interactive=False)
729
 
 
730
  with gr.Accordion("πŸ“ Register New Account", open=False):
731
  reg_username = gr.Textbox(label="Username")
732
  reg_password = gr.Textbox(label="Password", type="password")
 
736
 
737
  # ========= MAIN APP (hidden until login) =========
738
  with gr.Group(visible=False) as main_app:
739
+ # Profile header
740
+ with gr.Row():
741
+ avatar_display = gr.HTML()
742
+ user_display = gr.Textbox(label="Logged in as", interactive=False, visible=True)
743
+ logout_btn = gr.Button("⬅️ Logout", variant="secondary")
744
+
745
  with gr.Tabs():
746
  with gr.Tab("πŸ“’ Announcements"):
747
  gr.Markdown("### Filter by Course or Subject")
 
783
  submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
784
  clear_btn.click(lambda: [], None, chatbot)
785
 
 
786
  voice_btn.click(
787
  fn=voice_input_handler,
788
  inputs=None,
 
842
  outputs=file_list
843
  )
844
 
845
+ with gr.Tab("πŸ–ΌοΈ Profile"):
846
+ gr.Markdown("### πŸ–ΌοΈ Update Your Profile Picture")
847
+ with gr.Row():
848
+ avatar_input = gr.File(label="Choose an image (PNG, JPG)", file_types=["image"])
849
+ upload_avatar_btn = gr.Button("πŸ“€ Upload Avatar", variant="primary")
850
+ avatar_status = gr.Textbox(label="Status")
851
+ upload_avatar_btn.click(
852
+ fn=upload_avatar,
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,
860
+ student_service.students[CURRENT_USER]["name"] if CURRENT_USER != "guest" else "Guest"
861
+ ) if CURRENT_USER != "guest" else "",
862
+ inputs=None,
863
+ outputs=avatar_display
864
+ )
865
+
866
  with gr.Tab("πŸ” Teacher Admin"):
867
  gr.Markdown("### πŸ‘©β€πŸ« Post Announcements & View Stats")
868
 
 
909
  outputs=post_result
910
  )
911
 
912
+ # Logout button (already in header)
 
 
 
 
 
 
 
 
 
 
 
913
 
914
  # ========= EVENT HANDLERS =========
915
  login_btn.click(
 
917
  inputs=[login_username, login_password],
918
  outputs=[
919
  login_group, main_app, chatbot, gr.update(), assignments_display, groups_display,
920
+ login_status, user_display, logout_btn, avatar_display, dark_mode_btn, gr.update()
921
  ]
922
  )
923
 
 
927
  outputs=reg_status
928
  )
929
 
930
+ dark_mode_btn.click(
931
+ fn=toggle_dark_mode,
932
+ inputs=None,
933
+ outputs=[dark_mode_btn, gr.update()]
934
+ )
935
+
936
  # Launch app
937
  if __name__ == "__main__":
938
  demo.launch()