ngwakomadikwe commited on
Commit
e272769
Β·
verified Β·
1 Parent(s): a46cfbe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -283
app.py CHANGED
@@ -1,142 +1,27 @@
1
  """
2
- ThutoAI - Modern AI Student Assistant with Student Accounts & Multi-Language
3
- Meaning: "Thuto" = Learning/Education (Setswana)
4
 
5
- βœ… Student accounts: save chat history & files per user
6
- βœ… Multi-language: English / Setswana toggle
 
 
7
  βœ… Modern UI with dark mode, animations, teacher dashboard
8
  βœ… Fully commented for easy understanding
9
  """
10
 
11
  import os
12
  import gradio as gr
13
- from datetime import datetime
14
  from typing import List, Dict, Optional
15
  import time
16
  import json
17
-
18
- # ==================== LANGUAGE SUPPORT ====================
19
-
20
- class LanguageService:
21
- """Handles UI text in multiple languages."""
22
-
23
- def __init__(self):
24
- self.current_lang = "en" # Default: English
25
- self.translations = {
26
- "en": {
27
- "app_title": "πŸŽ“ ThutoAI β€” Your Modern AI Learning Assistant",
28
- "login_title": "πŸ” Student Login",
29
- "username_label": "Username",
30
- "password_label": "Password",
31
- "login_btn": "πŸ”“ Login",
32
- "register_btn": "πŸ“ Register",
33
- "logout_btn": "⬅️ Logout",
34
- "welcome": "Welcome back, {name}!",
35
- "register_success": "βœ… Account created! Please log in.",
36
- "invalid_login": "❌ Invalid username or password",
37
- "ask_placeholder": "E.g., How do I prepare for the Math exam?",
38
- "send_btn": "➀ Send",
39
- "clear_chat": "πŸ—‘οΈ Clear Chat",
40
- "upload_label": "Drag & drop or click to upload",
41
- "upload_btn": "πŸ“€ Upload",
42
- "file_uploaded": "βœ… Uploaded: {filename}",
43
- "no_file": "❌ No file selected",
44
- "announcements_title": "πŸ“’ Announcements",
45
- "filter_label": "Select Course",
46
- "refresh_btn": "πŸ”„ Refresh",
47
- "no_announcements": "πŸ“­ No announcements for this course.",
48
- "teacher_login_title": "πŸ‘©β€πŸ« Teacher Login",
49
- "post_announcement": "✍️ Create New Announcement",
50
- "announcement_title": "Title",
51
- "announcement_content": "Details for students...",
52
- "priority_label": "Priority",
53
- "post_btn": "πŸ“¬ Post Announcement",
54
- "posted_success": "βœ… Posted! πŸŽ‰ New announcement ID: {id}",
55
- "login_first": "πŸ”’ Please log in first.",
56
- "title_required": "⚠️ Title is required.",
57
- "content_required": "⚠️ Content is required.",
58
- "ai_thinking": "πŸ€” ThutoAI is thinking...",
59
- "ai_mock_tip": "πŸ’‘ Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!",
60
- "stats_title": "πŸ“Š Dashboard Stats",
61
- "total_announcements": "Total Announcements",
62
- "total_files": "Total Files Uploaded",
63
- "active_courses": "Active Courses",
64
- "high_priority": "High Priority Posts",
65
- "posted_by": "Posted by",
66
- "course": "Course",
67
- "date": "Date",
68
- "priority": "Priority",
69
- "dark_mode": "πŸŒ™ Toggle Dark Mode",
70
- "language_toggle": "🌐 Switch to Setswana",
71
- "back_to_english": "πŸ”€ Back to English"
72
- },
73
- "tn": { # Setswana
74
- "app_title": "πŸŽ“ ThutoAI β€” Motshwareledi wa Gago wa AI",
75
- "login_title": "πŸ” Tshedimosetso ya Moithuti",
76
- "username_label": "Leina la mo sebeletso",
77
- "password_label": "Password",
78
- "login_btn": "πŸ”“ Tshedimosa",
79
- "register_btn": "πŸ“ Ngolola",
80
- "logout_btn": "⬅️ Tsoha",
81
- "welcome": "La le amogatswe, {name}!",
82
- "register_success": "βœ… Ak'awunti e entsiwe! Tshedimosetse ka gonne.",
83
- "invalid_login": "❌ Leina le lebetsweng kapa password ga le sirele",
84
- "ask_placeholder": "Mohlala: Ke itumelelang jang mo go tlwaelo ya Math?",
85
- "send_btn": "➀ Romela",
86
- "clear_chat": "πŸ—‘οΈ Phumolosa Puisano",
87
- "upload_label": "Lemba & tsela kapa tobetsa go upload",
88
- "upload_btn": "πŸ“€ Upload",
89
- "file_uploaded": "βœ… E rometswe: {filename}",
90
- "no_file": "❌ Ga file e kgethilwe",
91
- "announcements_title": "πŸ“’ Ditlhaloso",
92
- "filter_label": "Kgetha Sebele",
93
- "refresh_btn": "πŸ”„ Nna gape",
94
- "no_announcements": "οΏ½οΏ½οΏ½ Ga go na ditlhaloso bakeng sa sebele seo.",
95
- "teacher_login_title": "πŸ‘©β€πŸ« Tshedimosetso ya Moruti",
96
- "post_announcement": "✍️ Bopa Tlhaloso e Ntsha",
97
- "announcement_title": "Thaetele",
98
- "announcement_content": "Tshedimosetso ya moithutwana...",
99
- "priority_label": "Boikanyo",
100
- "post_btn": "πŸ“¬ Romela Tlhaloso",
101
- "posted_success": "βœ… E rometswe! πŸŽ‰ Tlhaloso e ntsha ID: {id}",
102
- "login_first": "πŸ”’ Tshedimosetse pele.",
103
- "title_required": "⚠️ Thaetele e tlhofosegile.",
104
- "content_required": "⚠️ Tshedimosetso e tlhofosegile.",
105
- "ai_thinking": "πŸ€” ThutoAI e nang le maikutlo...",
106
- "ai_mock_tip": "πŸ’‘ Tip: Naya OpenAI API key mo HF Secrets bakeng sa ditlhahiso tse di botlhokwa!",
107
- "stats_title": "πŸ“Š Ditlhophiso tsa Dashboard",
108
- "total_announcements": "Ditlhaloso tsohle",
109
- "total_files": "Mafaele a rometsweng",
110
- "active_courses": "Disebele tse di sebetsang",
111
- "high_priority": "Diposo tse di ikanyeditsegile",
112
- "posted_by": "E rometswe ke",
113
- "course": "Sebele",
114
- "date": "Letsatsi",
115
- "priority": "Boikanyo",
116
- "dark_mode": "πŸŒ™ Nna Mode e E Nnyane",
117
- "language_toggle": "🌐 Tselang go ya Setswaneng",
118
- "back_to_english": "πŸ”€ Tselang go ya English"
119
- }
120
- }
121
-
122
- def set_language(self, lang_code: str):
123
- """Set current language."""
124
- if lang_code in self.translations:
125
- self.current_lang = lang_code
126
-
127
- def t(self, key: str, **kwargs) -> str:
128
- """Get translated text with optional formatting."""
129
- text = self.translations[self.current_lang].get(key, self.translations["en"].get(key, f"[{key}]"))
130
- return text.format(**kwargs) if kwargs else text
131
-
132
-
133
- # Initialize language service
134
- lang_service = LanguageService()
135
 
136
  # ==================== STUDENT ACCOUNTS SERVICE ====================
137
 
138
  class StudentService:
139
- """Manages student accounts, chat history, and files."""
140
 
141
  def __init__(self):
142
  # In-memory storage β€” replace with SQLite in production
@@ -144,7 +29,29 @@ class StudentService:
144
  "student1": {"password": "pass123", "name": "John Doe"},
145
  "student2": {"password": "pass456", "name": "Jane Smith"}
146
  }
147
- self.student_sessions = {} # {username: {chat_history: [], files: []}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  def register_student(self, username: str, password: str, name: str) -> str:
150
  """Register new student."""
@@ -153,47 +60,88 @@ class StudentService:
153
  if username in self.students:
154
  return "⚠️ Username already exists."
155
  self.students[username] = {"password": password, "name": name}
156
- self.student_sessions[username] = {"chat_history": [], "files": []}
157
- return lang_service.t("register_success")
 
 
 
 
 
158
 
159
  def authenticate_student(self, username: str, password: str) -> Optional[str]:
160
  """Authenticate student and return name if successful."""
161
  student = self.students.get(username)
162
  if student and student["password"] == password:
163
  if username not in self.student_sessions:
164
- self.student_sessions[username] = {"chat_history": [], "files": []}
 
 
 
 
 
165
  return student["name"]
166
  return None
167
 
168
  def get_chat_history(self, username: str) -> List:
169
- """Get chat history for student."""
170
  return self.student_sessions.get(username, {}).get("chat_history", [])
171
 
172
  def add_to_chat_history(self, username: str, user_msg: str, bot_reply: str):
173
- """Add message pair to student's chat history."""
174
  if username in self.student_sessions:
175
  self.student_sessions[username]["chat_history"].append((user_msg, bot_reply))
176
 
177
  def get_files(self, username: str) -> List:
178
- """Get uploaded files for student."""
179
  return self.student_sessions.get(username, {}).get("files", [])
180
 
181
  def add_file(self, username: str, filename: str):
182
- """Add file to student's list."""
183
  if username in self.student_sessions:
184
  self.student_sessions[username]["files"].append({
185
  "name": filename,
186
  "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
187
  })
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  # Initialize student service
191
  student_service = StudentService()
192
 
193
- # ==================== SCHOOL SERVICE (Enhanced) ====================
194
 
195
  class SchoolService:
196
- """Handles announcements, files, and AI context."""
197
 
198
  def __init__(self):
199
  self.announcements = [
@@ -262,7 +210,6 @@ class SchoolService:
262
  }
263
 
264
 
265
- # Initialize school service
266
  school_service = SchoolService()
267
 
268
  # ==================== ADMIN SERVICE ====================
@@ -305,7 +252,7 @@ def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
305
  return history, ""
306
 
307
  # Show "thinking" state
308
- thinking_msg = lang_service.t("ai_thinking")
309
  history.append((message, thinking_msg))
310
  yield history, ""
311
 
@@ -348,8 +295,8 @@ Guidelines:
348
  except Exception as e:
349
  reply = f"⚠️ Sorry, I had a glitch: {str(e)}"
350
  else:
351
- time.sleep(1.5) # Simulate thinking
352
- reply = f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\n{lang_service.t('ai_mock_tip')}"
353
 
354
  # Replace thinking message with real reply
355
  history[-1] = (message, reply)
@@ -361,18 +308,17 @@ Guidelines:
361
  yield history, ""
362
 
363
 
364
- # ==================== UI RENDERING ====================
365
 
366
- def render_announcements(course: str, lang: str = "en") -> str:
367
  """Render announcements with modern cards."""
368
- lang_service.set_language(lang)
369
  announcements = school_service.get_announcements(course)
370
  if not announcements:
371
- return f"""
372
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
373
  <div style='font-size: 4em; margin-bottom: 16px;'>πŸ“­</div>
374
- <h3>{lang_service.t("no_announcements")}</h3>
375
- <p>{lang_service.t("refresh_btn")} {lang_service.t("filter_label").lower()}</p>
376
  </div>
377
  """
378
 
@@ -420,25 +366,124 @@ def render_announcements(course: str, lang: str = "en") -> str:
420
  return html
421
 
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  # ==================== STATE MANAGEMENT ====================
424
 
425
  CURRENT_USER = "guest"
426
- CURRENT_LANG = "en"
427
 
428
  def login_student(username: str, password: str) -> tuple:
429
  """Login student and load their data."""
430
- global CURRENT_USER, CURRENT_LANG
431
  student_name = student_service.authenticate_student(username, password)
432
  if student_name:
433
  CURRENT_USER = username
434
  chat_history = student_service.get_chat_history(username)
435
  files = student_service.get_files(username)
436
- welcome_msg = lang_service.t("welcome", name=student_name)
 
 
437
  return (
438
  gr.update(visible=False), # Hide login
439
  gr.update(visible=True), # Show main UI
440
  chat_history,
441
  files,
 
 
442
  welcome_msg,
443
  gr.update(value=student_name, visible=True),
444
  gr.update(visible=True) # Show logout
@@ -448,17 +493,17 @@ def login_student(username: str, password: str) -> tuple:
448
  gr.update(visible=False),
449
  [],
450
  [],
451
- lang_service.t("invalid_login"),
 
 
452
  gr.update(visible=False),
453
  gr.update(visible=False)
454
  )
455
 
456
  def register_student(username: str, password: str, name: str) -> str:
457
- """Register new student."""
458
  return student_service.register_student(username, password, name)
459
 
460
  def logout_student() -> tuple:
461
- """Logout student."""
462
  global CURRENT_USER
463
  CURRENT_USER = "guest"
464
  return (
@@ -467,56 +512,66 @@ def logout_student() -> tuple:
467
  [],
468
  [],
469
  "",
 
 
470
  gr.update(visible=False),
471
  gr.update(visible=False)
472
  )
473
 
474
- def switch_language(lang_choice: str) -> tuple:
475
- """Switch UI language."""
476
- global CURRENT_LANG
477
- lang_code = "tn" if "Setswana" in lang_choice else "en"
478
- CURRENT_LANG = lang_code
479
- lang_service.set_language(lang_code)
480
-
481
- # Return translated UI elements
482
- new_title = lang_service.t("app_title")
483
- new_login_title = lang_service.t("login_title")
484
- new_lang_btn_text = lang_service.t("back_to_english") if lang_code == "tn" else lang_service.t("language_toggle")
485
-
486
- return (
487
- new_title,
488
- new_login_title,
489
- new_lang_btn_text,
490
- lang_service.t("username_label"),
491
- lang_service.t("password_label"),
492
- lang_service.t("login_btn"),
493
- lang_service.t("register_btn"),
494
- lang_service.t("ask_placeholder"),
495
- lang_service.t("send_btn"),
496
- lang_service.t("clear_chat"),
497
- lang_service.t("upload_label"),
498
- lang_service.t("upload_btn"),
499
- lang_service.t("announcements_title"),
500
- lang_service.t("filter_label"),
501
- lang_service.t("refresh_btn"),
502
- lang_service.t("teacher_login_title"),
503
- lang_service.t("post_announcement"),
504
- lang_service.t("announcement_title"),
505
- lang_service.t("announcement_content"),
506
- lang_service.t("priority_label"),
507
- lang_service.t("post_btn"),
508
- lang_service.t("dark_mode")
509
- )
510
-
511
- def upload_file_for_student(file, username: str = "guest") -> str:
512
- """Handle file upload for student."""
513
  if not file:
514
- return lang_service.t("no_file")
515
- result = f"{lang_service.t('file_uploaded', filename=file.name)}"
516
- if username != "guest":
517
- student_service.add_file(username, file.name)
518
  return result
519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  # ==================== TEACHER FUNCTIONS ====================
521
 
522
  IS_ADMIN = False
@@ -526,7 +581,7 @@ def admin_login(username: str, password: str) -> tuple:
526
  if admin_service.authenticate(username, password):
527
  IS_ADMIN = True
528
  stats = school_service.get_stats()
529
- stats_text = f"{lang_service.t('stats_title')}: {stats['total_announcements']} {lang_service.t('total_announcements').lower()}, {stats['total_files']} {lang_service.t('total_files').lower()}"
530
  return (
531
  gr.update(visible=False),
532
  gr.update(visible=True),
@@ -536,7 +591,7 @@ def admin_login(username: str, password: str) -> tuple:
536
  return (
537
  gr.update(visible=True),
538
  gr.update(visible=False),
539
- lang_service.t("invalid_login"),
540
  gr.update(visible=False)
541
  )
542
 
@@ -552,14 +607,14 @@ def admin_logout():
552
 
553
  def post_announcement(title: str, content: str, course: str, priority: str) -> str:
554
  if not IS_ADMIN:
555
- return lang_service.t("login_first")
556
  if not title.strip():
557
- return lang_service.t("title_required")
558
  if not content.strip():
559
- return lang_service.t("content_required")
560
 
561
  school_service.add_announcement(title, content, course, priority)
562
- return lang_service.t("posted_success", id=len(school_service.announcements))
563
 
564
  # ==================== CUSTOM CSS ====================
565
 
@@ -597,34 +652,23 @@ CUSTOM_CSS = """
597
  # ==================== BUILD UI ====================
598
 
599
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
600
- # State variables
601
- username_state = gr.State("guest")
602
- lang_state = gr.State("en")
603
-
604
- # Header with language toggle
605
- with gr.Row():
606
- title_display = gr.Markdown(f"# {lang_service.t('app_title')}")
607
- lang_choice = gr.Dropdown(
608
- choices=["🌐 English", "🌐 Setswana"],
609
- value="🌐 English",
610
- label="Language",
611
- scale=1
612
- )
613
 
614
  # Dark mode button
615
- dark_mode_btn = gr.Button(lang_service.t("dark_mode"), variant="secondary")
616
 
617
  # ========= STUDENT LOGIN/REGISTER =========
618
  with gr.Group() as login_group:
619
- gr.Markdown(f"### {lang_service.t('login_title')}")
620
  with gr.Row():
621
- login_username = gr.Textbox(label=lang_service.t("username_label"), scale=3)
622
- login_password = gr.Textbox(label=lang_service.t("password_label"), type="password", scale=3)
623
- login_btn = gr.Button(lang_service.t("login_btn"), variant="primary")
624
- register_btn = gr.Button(lang_service.t("register_btn"), variant="secondary")
625
  login_status = gr.Textbox(label="Status", interactive=False)
626
 
627
- # Register modal (simplified)
628
  with gr.Accordion("πŸ“ Register New Account", open=False):
629
  reg_username = gr.Textbox(label="Username")
630
  reg_password = gr.Textbox(label="Password", type="password")
@@ -635,32 +679,20 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
635
  # ========= MAIN APP (hidden until login) =========
636
  with gr.Group(visible=False) as main_app:
637
  with gr.Tabs():
638
- with gr.Tab(lang_service.t("announcements_title")):
639
- gr.Markdown(f"### {lang_service.t('announcements_title')}")
640
  with gr.Row():
641
  course_filter = gr.Dropdown(
642
  choices=school_service.courses,
643
  value="All",
644
- label=lang_service.t("filter_label"),
645
  scale=3
646
  )
647
- refresh_btn = gr.Button(lang_service.t("refresh_btn"), variant="secondary", scale=1)
648
  announcements_html = gr.HTML()
649
- course_filter.change(
650
- fn=lambda c: render_announcements(c, CURRENT_LANG),
651
- inputs=course_filter,
652
- outputs=announcements_html
653
- )
654
- refresh_btn.click(
655
- fn=lambda c: render_announcements(c, CURRENT_LANG),
656
- inputs=course_filter,
657
- outputs=announcements_html
658
- )
659
- demo.load(
660
- fn=lambda c: render_announcements(c, CURRENT_LANG),
661
- inputs=course_filter,
662
- outputs=announcements_html
663
- )
664
 
665
  with gr.Tab("πŸ’¬ Ask ThutoAI"):
666
  gr.Markdown("### πŸ’‘ Ask me anything β€” I'm here to help!")
@@ -671,15 +703,15 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
671
  )
672
  with gr.Row():
673
  msg = gr.Textbox(
674
- label="Your Question",
675
- placeholder=lang_service.t("ask_placeholder"),
676
- scale=8
677
  )
678
- submit_btn = gr.Button(lang_service.t("send_btn"), variant="primary", scale=1)
679
- clear_btn = gr.Button(lang_service.t("clear_chat"), variant="secondary")
 
680
 
681
  def respond(message, chat_history):
682
- # Use generator for streaming-like effect
683
  for updated_history, _ in ai_chat(message, chat_history, CURRENT_USER):
684
  yield updated_history, ""
685
 
@@ -687,20 +719,60 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
687
  submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
688
  clear_btn.click(lambda: [], None, chatbot)
689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  with gr.Tab("πŸ“‚ My Files"):
691
- gr.Markdown("### πŸ“ Your Uploaded Files")
692
  with gr.Row():
693
- file_input = gr.File(label=lang_service.t("upload_label"), elem_classes=["file-upload"])
694
- upload_btn = gr.Button(lang_service.t("upload_btn"), variant="primary")
695
  upload_status = gr.Textbox(label="Status")
696
  file_list = gr.JSON(label="Your Files")
697
 
698
  upload_btn.click(
699
- fn=lambda f: upload_file_for_student(f, CURRENT_USER),
700
  inputs=file_input,
701
  outputs=upload_status
702
  )
703
- # Update file list when tab is opened (simplified)
704
  demo.load(
705
  fn=lambda: student_service.get_files(CURRENT_USER) if CURRENT_USER != "guest" else [],
706
  inputs=None,
@@ -708,25 +780,34 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
708
  )
709
 
710
  with gr.Tab("πŸ” Teacher Admin"):
711
- gr.Markdown(f"### {lang_service.t('teacher_login_title')}")
712
 
713
  with gr.Group() as teacher_login_group:
714
- teacher_username = gr.Textbox(label=lang_service.t("username_label"))
715
- teacher_password = gr.Textbox(label=lang_service.t("password_label"), type="password")
716
- teacher_login_btn = gr.Button(lang_service.t("login_btn"), variant="primary")
717
  teacher_status = gr.Textbox(label="Status")
718
 
719
  with gr.Group(visible=False) as teacher_dashboard:
720
- gr.Markdown(f"### {lang_service.t('post_announcement')}")
 
 
 
 
 
 
 
 
 
721
  with gr.Row():
722
- ann_title = gr.Textbox(label=lang_service.t("announcement_title"), scale=3)
723
- ann_course = gr.Dropdown(choices=school_service.courses[1:], label=lang_service.t("course"), value="General", scale=2)
724
- ann_content = gr.Textbox(label=lang_service.t("announcement_content"), lines=3)
725
- ann_priority = gr.Radio(["low", "normal", "high"], label=lang_service.t("priority_label"), value="normal")
726
- post_btn = gr.Button(lang_service.t("post_btn"), variant="primary")
727
  post_result = gr.Textbox(label="Result")
728
 
729
- teacher_logout_btn = gr.Button(lang_service.t("logout_btn"), variant="secondary")
730
 
731
  teacher_login_btn.click(
732
  fn=admin_login,
@@ -747,12 +828,12 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
747
  # Logout button
748
  with gr.Row():
749
  user_display = gr.Textbox(label="Logged in as", interactive=False, visible=False)
750
- logout_btn = gr.Button(lang_service.t("logout_btn"), variant="secondary", visible=False)
751
  logout_btn.click(
752
  fn=logout_student,
753
  inputs=None,
754
  outputs=[
755
- login_group, main_app, chatbot, file_list,
756
  login_status, user_display, logout_btn
757
  ]
758
  )
@@ -762,7 +843,7 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
762
  fn=login_student,
763
  inputs=[login_username, login_password],
764
  outputs=[
765
- login_group, main_app, chatbot, file_list,
766
  login_status, user_display, logout_btn
767
  ]
768
  )
@@ -773,26 +854,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as de
773
  outputs=reg_status
774
  )
775
 
776
- lang_choice.change(
777
- fn=switch_language,
778
- inputs=lang_choice,
779
- outputs=[
780
- title_display,
781
- # Update all translatable elements
782
- gr.update(), # login_title (Markdown)
783
- lang_choice, # update button text
784
- login_username, login_password, login_btn, register_btn,
785
- msg, submit_btn, clear_btn,
786
- file_input, upload_btn,
787
- gr.update(), # announcements_title (Tab)
788
- course_filter, refresh_btn,
789
- gr.update(), # teacher_login_title (Markdown)
790
- gr.update(), # post_announcement (Markdown)
791
- ann_title, ann_content, ann_priority, post_btn,
792
- dark_mode_btn
793
- ]
794
- )
795
-
796
  # Launch app
797
  if __name__ == "__main__":
798
  demo.launch()
 
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
14
  import gradio as gr
15
+ 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
 
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": [],
36
+ "files": [],
37
+ "assignments": [
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": [],
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
+ }
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."""
 
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:
76
+ self.student_sessions[username] = {
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
 
88
  def add_to_chat_history(self, username: str, user_msg: str, bot_reply: str):
 
89
  if username in self.student_sessions:
90
  self.student_sessions[username]["chat_history"].append((user_msg, bot_reply))
91
 
92
  def get_files(self, username: str) -> List:
 
93
  return self.student_sessions.get(username, {}).get("files", [])
94
 
95
  def add_file(self, username: str, filename: str):
 
96
  if username in self.student_sessions:
97
  self.student_sessions[username]["files"].append({
98
  "name": filename,
99
  "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
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
+
121
+ def join_group(self, username: str, group_code: str) -> str:
122
+ if group_code.upper() not in self.valid_groups:
123
+ return "❌ Invalid group code. Ask your teacher for the correct code."
124
+ if username in self.student_sessions:
125
+ if group_code.upper() not in self.student_sessions[username]["groups"]:
126
+ self.student_sessions[username]["groups"].append(group_code.upper())
127
+ return f"βœ… Joined group: {group_code.upper()}"
128
+ return "❌ Login required."
129
+
130
+ def leave_group(self, username: str, group_code: str) -> str:
131
+ if username in self.student_sessions:
132
+ if group_code.upper() in self.student_sessions[username]["groups"]:
133
+ self.student_sessions[username]["groups"].remove(group_code.upper())
134
+ return f"βœ… Left group: {group_code.upper()}"
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 = [
 
210
  }
211
 
212
 
 
213
  school_service = SchoolService()
214
 
215
  # ==================== ADMIN SERVICE ====================
 
252
  return history, ""
253
 
254
  # Show "thinking" state
255
+ thinking_msg = "πŸ€” ThutoAI is thinking..."
256
  history.append((message, thinking_msg))
257
  yield history, ""
258
 
 
295
  except Exception as e:
296
  reply = f"⚠️ Sorry, I had a glitch: {str(e)}"
297
  else:
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)
 
308
  yield history, ""
309
 
310
 
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 """
318
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
319
  <div style='font-size: 4em; margin-bottom: 16px;'>πŸ“­</div>
320
+ <h3>No announcements for this course.</h3>
321
+ <p>Check back later or select "All" to see everything.</p>
322
  </div>
323
  """
324
 
 
366
  return html
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;'>
374
+ <div style='font-size: 4em; margin-bottom: 16px;'>πŸ“…</div>
375
+ <h3>No upcoming assignments.</h3>
376
+ <p>Ask your teacher or join a class group to see assignments.</p>
377
+ </div>
378
+ """
379
+
380
+ html = "<div style='display: grid; gap: 16px;'>"
381
+ today = datetime.today().date()
382
+
383
+ for task in assignments:
384
+ due_date = datetime.strptime(task["due_date"], "%Y-%m-%d").date()
385
+ days_left = (due_date - today).days
386
+ is_overdue = days_left < 0
387
+ is_today = days_left == 0
388
+
389
+ if is_overdue:
390
+ badge = "🚨 OVERDUE"
391
+ color = "#dc3545"
392
+ elif is_today:
393
+ badge = "🎯 TODAY"
394
+ color = "#fd7e14"
395
+ elif days_left <= 2:
396
+ badge = f"⚠️ Due in {days_left} day{'s' if days_left != 1 else ''}"
397
+ color = "#ffc107"
398
+ else:
399
+ badge = f"βœ… Due in {days_left} days"
400
+ color = "#28a745"
401
+
402
+ html += f"""
403
+ <div style='
404
+ background: white;
405
+ border-radius: 12px;
406
+ padding: 20px;
407
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
408
+ border-left: 4px solid {color};
409
+ transition: transform 0.2s;
410
+ '>
411
+ <div style='display: flex; justify-content: space-between; align-items: flex-start;'>
412
+ <div>
413
+ <h3 style='margin: 0 0 8px 0; color: #212529;'>{task['title']}</h3>
414
+ <div style='color: #6c757d; margin-bottom: 8px;'>πŸ“š {task['course']}</div>
415
+ <div style='color: #495057;'>πŸ“… Due: {task['due_date']}</div>
416
+ </div>
417
+ <span style='
418
+ background: {color};
419
+ color: white;
420
+ padding: 6px 12px;
421
+ border-radius: 20px;
422
+ font-weight: bold;
423
+ font-size: 0.85em;
424
+ align-self: flex-start;
425
+ '>{badge}</span>
426
+ </div>
427
+ </div>
428
+ """
429
+
430
+ html += "</div>"
431
+ return html
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;'>
439
+ <div style='font-size: 4em; margin-bottom: 16px;'>πŸ‘₯</div>
440
+ <h3>You haven't joined any class groups yet.</h3>
441
+ <p>Ask your teacher for a group code to join your class.</p>
442
+ </div>
443
+ """
444
+
445
+ html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;'>"
446
+ for group in groups:
447
+ html += f"""
448
+ <div style='
449
+ background: #e3f2fd;
450
+ border-radius: 12px;
451
+ padding: 20px;
452
+ text-align: center;
453
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
454
+ border: 2px solid #2196f3;
455
+ '>
456
+ <div style='font-size: 1.5em; margin-bottom: 8px;'>πŸŽ’</div>
457
+ <h3 style='margin: 0; color: #1976d2;'>{group}</h3>
458
+ <p style='margin: 8px 0 0 0; color: #555; font-size: 0.9em;'>Class Group</p>
459
+ </div>
460
+ """
461
+ html += "</div>"
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
 
493
  gr.update(visible=False),
494
  [],
495
  [],
496
+ "",
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 (
 
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.", ""
524
+ result = student_service.join_group(CURRENT_USER, group_code)
525
+ groups = student_service.get_groups(CURRENT_USER)
526
+ return result, render_groups(groups)
527
+
528
+ def leave_group(group_code: str) -> tuple:
529
+ if CURRENT_USER == "guest":
530
+ return "❌ Please log in first.", ""
531
+ result = student_service.leave_group(CURRENT_USER, group_code)
532
+ groups = student_service.get_groups(CURRENT_USER)
533
+ return result, render_groups(groups)
534
+
535
+ def upload_file_for_student(file) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  if not file:
537
+ return "❌ No file selected"
538
+ result = f"βœ… Uploaded: {file.name}"
539
+ if CURRENT_USER != "guest":
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() {
547
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
548
+ alert('πŸŽ™οΈ Voice input is not supported in your browser. Try Chrome or Edge.');
549
+ return '';
550
+ }
551
+
552
+ const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
553
+ recognition.lang = 'en-US';
554
+ recognition.interimResults = false;
555
+ recognition.maxAlternatives = 1;
556
+
557
+ return new Promise((resolve) => {
558
+ recognition.start();
559
+ recognition.onresult = (event) => {
560
+ const transcript = event.results[0][0].transcript;
561
+ resolve(transcript);
562
+ };
563
+ recognition.onerror = (event) => {
564
+ alert('πŸŽ™οΈ Error: ' + event.error);
565
+ resolve('');
566
+ };
567
+ });
568
+ }
569
+ """
570
+
571
+ def voice_input_handler() -> str:
572
+ """Placeholder function β€” actual voice handled by JS."""
573
+ return ""
574
+
575
  # ==================== TEACHER FUNCTIONS ====================
576
 
577
  IS_ADMIN = False
 
581
  if admin_service.authenticate(username, password):
582
  IS_ADMIN = True
583
  stats = school_service.get_stats()
584
+ stats_text = f"πŸ“Š Stats: {stats['total_announcements']} announcements, {stats['total_files']} files"
585
  return (
586
  gr.update(visible=False),
587
  gr.update(visible=True),
 
591
  return (
592
  gr.update(visible=True),
593
  gr.update(visible=False),
594
+ "❌ Invalid credentials",
595
  gr.update(visible=False)
596
  )
597
 
 
607
 
608
  def post_announcement(title: str, content: str, course: str, priority: str) -> str:
609
  if not IS_ADMIN:
610
+ return "πŸ”’ Please log in first."
611
  if not title.strip():
612
+ return "⚠️ Title is required."
613
  if not content.strip():
614
+ return "⚠️ Content is required."
615
 
616
  school_service.add_announcement(title, content, course, priority)
617
+ return f"βœ… Posted! πŸŽ‰ New announcement ID: {len(school_service.announcements)}"
618
 
619
  # ==================== CUSTOM CSS ====================
620
 
 
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:
663
+ gr.Markdown("### πŸ” Student Login")
664
  with gr.Row():
665
+ login_username = gr.Textbox(label="Username", scale=3)
666
+ login_password = gr.Textbox(label="Password", type="password", scale=3)
667
+ login_btn = gr.Button("πŸ”“ Login", variant="primary")
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")
 
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")
684
  with gr.Row():
685
  course_filter = gr.Dropdown(
686
  choices=school_service.courses,
687
  value="All",
688
+ label="Select Course",
689
  scale=3
690
  )
691
+ refresh_btn = gr.Button("πŸ”„ Refresh", variant="secondary", scale=1)
692
  announcements_html = gr.HTML()
693
+ course_filter.change(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
694
+ refresh_btn.click(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
695
+ demo.load(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
  with gr.Tab("πŸ’¬ Ask ThutoAI"):
698
  gr.Markdown("### πŸ’‘ Ask me anything β€” I'm here to help!")
 
703
  )
704
  with gr.Row():
705
  msg = gr.Textbox(
706
+ label="Type your question",
707
+ placeholder="E.g., How do I prepare for the Math exam?",
708
+ scale=7
709
  )
710
+ voice_btn = gr.Button("πŸŽ™οΈ Speak", variant="secondary", scale=1)
711
+ submit_btn = gr.Button("➀ Send", variant="primary", scale=1)
712
+ clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", variant="secondary")
713
 
714
  def respond(message, chat_history):
 
715
  for updated_history, _ in ai_chat(message, chat_history, CURRENT_USER):
716
  yield updated_history, ""
717
 
 
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,
726
+ outputs=msg,
727
+ js=VOICE_JS + "return startVoiceInput();"
728
+ )
729
+
730
+ with gr.Tab("πŸ“… Assignments"):
731
+ gr.Markdown("### πŸ“Œ Your Upcoming Assignments & Exams")
732
+ assignments_display = gr.HTML()
733
+ demo.load(
734
+ fn=lambda: render_assignments(student_service.get_assignments(CURRENT_USER)) if CURRENT_USER != "guest" else "",
735
+ inputs=None,
736
+ outputs=assignments_display
737
+ )
738
+
739
+ with gr.Tab("πŸ‘₯ Class Groups"):
740
+ gr.Markdown("### πŸŽ’ Join Your Class Groups")
741
+ with gr.Row():
742
+ group_code_input = gr.Textbox(label="Group Code (e.g., MATH10A)", scale=3)
743
+ join_btn = gr.Button("βž• Join Group", variant="primary", scale=1)
744
+ leave_btn = gr.Button("βž– Leave Group", variant="secondary", scale=1)
745
+ group_status = gr.Textbox(label="Status")
746
+ groups_display = gr.HTML()
747
+ join_btn.click(
748
+ fn=join_group,
749
+ inputs=group_code_input,
750
+ outputs=[group_status, groups_display]
751
+ )
752
+ leave_btn.click(
753
+ fn=leave_group,
754
+ inputs=group_code_input,
755
+ outputs=[group_status, groups_display]
756
+ )
757
+ demo.load(
758
+ fn=lambda: render_groups(student_service.get_groups(CURRENT_USER)) if CURRENT_USER != "guest" else "",
759
+ inputs=None,
760
+ outputs=groups_display
761
+ )
762
+
763
  with gr.Tab("πŸ“‚ My Files"):
764
+ gr.Markdown("### πŸ“ Upload & Manage Study Materials")
765
  with gr.Row():
766
+ file_input = gr.File(label="Drag & drop or click to upload", elem_classes=["file-upload"])
767
+ upload_btn = gr.Button("πŸ“€ Upload", variant="primary")
768
  upload_status = gr.Textbox(label="Status")
769
  file_list = gr.JSON(label="Your Files")
770
 
771
  upload_btn.click(
772
+ fn=upload_file_for_student,
773
  inputs=file_input,
774
  outputs=upload_status
775
  )
 
776
  demo.load(
777
  fn=lambda: student_service.get_files(CURRENT_USER) if CURRENT_USER != "guest" else [],
778
  inputs=None,
 
780
  )
781
 
782
  with gr.Tab("πŸ” Teacher Admin"):
783
+ gr.Markdown("### πŸ‘©β€πŸ« Post Announcements & View Stats")
784
 
785
  with gr.Group() as teacher_login_group:
786
+ teacher_username = gr.Textbox(label="Username")
787
+ teacher_password = gr.Textbox(label="Password", type="password")
788
+ teacher_login_btn = gr.Button("πŸ”“ Login", variant="primary")
789
  teacher_status = gr.Textbox(label="Status")
790
 
791
  with gr.Group(visible=False) as teacher_dashboard:
792
+ gr.Markdown("### πŸ“Š Dashboard Stats")
793
+ stats = school_service.get_stats()
794
+ gr.Markdown(f"""
795
+ - πŸ“’ Total Announcements: **{stats['total_announcements']}**
796
+ - πŸ“ Total Files Uploaded: **{stats['total_files']}**
797
+ - 🎯 Active Courses: **{stats['active_courses']}**
798
+ - 🚨 High Priority Posts: **{stats['high_priority']}**
799
+ """)
800
+
801
+ gr.Markdown("### ✍️ Create New Announcement")
802
  with gr.Row():
803
+ ann_title = gr.Textbox(label="Title", placeholder="e.g., Quiz Moved to Friday", scale=3)
804
+ ann_course = gr.Dropdown(choices=school_service.courses[1:], label="Course", value="General", scale=2)
805
+ ann_content = gr.Textbox(label="Content", placeholder="Details for students...", lines=3)
806
+ ann_priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal", inline=True)
807
+ post_btn = gr.Button("πŸ“¬ Post Announcement", variant="primary")
808
  post_result = gr.Textbox(label="Result")
809
 
810
+ teacher_logout_btn = gr.Button("⬅️ Logout", variant="secondary")
811
 
812
  teacher_login_btn.click(
813
  fn=admin_login,
 
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
  )
 
843
  fn=login_student,
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
  )
 
854
  outputs=reg_status
855
  )
856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  # Launch app
858
  if __name__ == "__main__":
859
  demo.launch()