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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +512 -213
app.py CHANGED
@@ -1,22 +1,196 @@
1
  """
2
- ThutoAI - Modern AI Student Assistant (Gradio Enhanced)
3
  Meaning: "Thuto" = Learning/Education (Setswana)
4
 
5
- βœ… Modern UI with animations, dark mode, icons
6
- βœ… Mobile-first responsive design
7
- βœ… Teacher dashboard with stats
8
- βœ… Loading states & smooth UX
9
- βœ… File upload preview & management
10
  βœ… Fully commented for easy understanding
11
  """
12
 
13
  import os
14
  import gradio as gr
15
  from datetime import datetime
16
- from typing import List, Dict
17
  import time
 
18
 
19
- # ==================== MOCK SERVICES (Replace with DB later) ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  class SchoolService:
22
  """Handles announcements, files, and AI context."""
@@ -52,7 +226,6 @@ class SchoolService:
52
  },
53
  ]
54
  self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
55
- self.files = []
56
  self.total_announcements = len(self.announcements)
57
  self.total_files = 0
58
 
@@ -74,21 +247,9 @@ class SchoolService:
74
  })
75
  self.total_announcements += 1
76
 
77
- def upload_file(self, file) -> str:
78
- if not file:
79
- return "❌ No file selected"
80
- filename = file.name
81
- self.files.append({
82
- "name": filename,
83
- "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
84
- "size": f"{os.path.getsize(file.name) // 1024} KB" if os.path.exists(file.name) else "Unknown"
85
- })
86
- self.total_files += 1
87
- return f"βœ… Uploaded: {filename}"
88
-
89
  def get_school_context_for_ai(self) -> str:
90
  context = "Current School Announcements:\n"
91
- for ann in self.announcements[-5:]: # Show latest 5
92
  context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:80]}... (Priority: {ann['priority']})\n"
93
  return context
94
 
@@ -101,6 +262,11 @@ class SchoolService:
101
  }
102
 
103
 
 
 
 
 
 
104
  class AdminService:
105
  """Handles teacher authentication."""
106
 
@@ -114,11 +280,10 @@ class AdminService:
114
  return self.admins.get(username) == password
115
 
116
 
117
- # Initialize services
118
- school_service = SchoolService()
119
  admin_service = AdminService()
120
 
121
- # OpenAI setup
 
122
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
123
  USE_OPENAI = bool(OPENAI_API_KEY)
124
 
@@ -132,12 +297,17 @@ if USE_OPENAI:
132
  else:
133
  print("⚠️ OPENAI_API_KEY not set. Using mock responses.")
134
 
135
- # ==================== AI CHAT WITH LOADING STATE ====================
136
 
137
- def ai_chat(message: str, history: List) -> str:
138
- """Generate AI response with loading animation."""
139
  if not message.strip():
140
- return "⚠️ Please ask a real question."
 
 
 
 
 
141
 
142
  if USE_OPENAI:
143
  try:
@@ -152,7 +322,6 @@ Guidelines:
152
  - Offer study tips if appropriate.
153
  - Never invent facts β€” say 'I don't know' if unsure."""
154
 
155
- # βœ… Fixed: Properly closed parentheses
156
  response = client.chat.completions.create(
157
  model="gpt-3.5-turbo",
158
  messages=[
@@ -176,26 +345,34 @@ Guidelines:
176
  emoji = "🚨" if ann["priority"] == "high" else "πŸ“Œ" if ann["priority"] == "normal" else "ℹ️"
177
  reply += f"\n{emoji} **{ann['title']}** ({ann['course']})\n β†’ {ann['content'][:70]}..."
178
 
179
- return reply
180
-
181
  except Exception as e:
182
- return f"⚠️ Sorry, I had a glitch: {str(e)}"
183
  else:
184
- time.sleep(1.5) # Simulate AI thinking
185
- return f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\nπŸ’‘ *Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!*"
 
 
 
 
 
 
 
186
 
 
187
 
188
- # ==================== MODERN UI RENDERING ====================
189
 
190
- def render_announcements(course: str) -> str:
191
- """Render announcements with modern cards and icons."""
 
 
 
192
  announcements = school_service.get_announcements(course)
193
  if not announcements:
194
- return """
195
  <div style='text-align: center; padding: 40px; color: #6c757d;'>
196
  <div style='font-size: 4em; margin-bottom: 16px;'>πŸ“­</div>
197
- <h3>No announcements for this course.</h3>
198
- <p>Check back later or select "All" to see everything.</p>
199
  </div>
200
  """
201
 
@@ -214,9 +391,7 @@ def render_announcements(course: str) -> str:
214
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
215
  border-left: 4px solid {color};
216
  transition: transform 0.2s, box-shadow 0.2s;
217
- animation: fadeInUp 0.6s ease-out;
218
- ' onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.1)';"
219
- onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)';">
220
  <div style='display: flex; align-items: flex-start; gap: 12px;'>
221
  <div style='font-size: 1.5em;'>{icon}</div>
222
  <div style='flex: 1;'>
@@ -241,53 +416,131 @@ def render_announcements(course: str) -> str:
241
  </div>
242
  </div>
243
  """
244
-
245
  html += "</div>"
246
-
247
- # Add animation CSS
248
- html += """
249
- <style>
250
- @keyframes fadeInUp {
251
- from {
252
- opacity: 0;
253
- transform: translateY(20px);
254
- }
255
- to {
256
- opacity: 1;
257
- transform: translateY(0);
258
- }
259
- }
260
- </style>
261
- """
262
  return html
263
 
264
 
265
- # ==================== STATE & ADMIN FUNCTIONS ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  IS_ADMIN = False
268
 
269
  def admin_login(username: str, password: str) -> tuple:
270
- """Handle admin login with feedback."""
271
  global IS_ADMIN
272
  if admin_service.authenticate(username, password):
273
  IS_ADMIN = True
274
  stats = school_service.get_stats()
275
- stats_text = f"πŸ“Š Stats: {stats['total_announcements']} announcements, {stats['total_files']} files"
276
  return (
277
  gr.update(visible=False),
278
  gr.update(visible=True),
279
- f"βœ… Welcome back, {username.split('@')[0].title()}! {stats_text}",
280
  gr.update(visible=True)
281
  )
282
  return (
283
  gr.update(visible=True),
284
  gr.update(visible=False),
285
- "❌ Invalid credentials. Try [email protected] / password123",
286
  gr.update(visible=False)
287
  )
288
 
289
  def admin_logout():
290
- """Handle admin logout."""
291
  global IS_ADMIN
292
  IS_ADMIN = False
293
  return (
@@ -298,41 +551,30 @@ def admin_logout():
298
  )
299
 
300
  def post_announcement(title: str, content: str, course: str, priority: str) -> str:
301
- """Post announcement with validation."""
302
  if not IS_ADMIN:
303
- return "πŸ”’ Please log in first."
304
  if not title.strip():
305
- return "⚠️ Title is required."
306
  if not content.strip():
307
- return "⚠️ Content is required."
308
 
309
  school_service.add_announcement(title, content, course, priority)
310
- return f"βœ… Posted! πŸŽ‰ New announcement ID: {len(school_service.announcements)}"
311
 
312
- # ==================== CUSTOM CSS & JS FOR MODERN UI ====================
313
 
314
  CUSTOM_CSS = """
315
- /* Modern, clean design system */
316
  .gradio-container {
317
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
318
  max-width: 1200px;
319
  margin: 0 auto;
320
  padding: 16px;
321
  }
322
-
323
- /* Button styles */
324
  .primary {
325
  background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
326
  border: none !important;
327
  color: white !important;
328
- transition: all 0.3s ease !important;
329
  }
330
- .primary:hover {
331
- transform: translateY(-2px) !important;
332
- box-shadow: 0 4px 12px rgba(110, 142, 251, 0.3) !important;
333
- }
334
-
335
- /* Chatbot bubble styling */
336
  .chatbot-container {
337
  background: #f8f9fa !important;
338
  border-radius: 16px !important;
@@ -340,159 +582,216 @@ CUSTOM_CSS = """
340
  .user, .bot {
341
  border-radius: 18px !important;
342
  padding: 12px 16px !important;
343
- margin: 4px 0 !important;
344
- }
345
- .bot {
346
- background: linear-gradient(135deg, #f5f7fa, #e4e8eb) !important;
347
- border: 1px solid #e0e0e0 !important;
348
  }
349
-
350
- /* File upload area */
351
  .file-upload {
352
  border: 2px dashed #6e8efb !important;
353
  border-radius: 12px !important;
354
  background: #f8f9ff !important;
355
  }
356
-
357
- /* Dark mode toggle */
358
  .dark-mode {
359
  background: #1e1e1e !important;
360
  color: #f0f0f0 !important;
361
  }
362
- .dark-mode .gr-box {
363
- background: #2d2d2d !important;
364
- border-color: #444 !important;
365
- }
366
- .dark-mode .gr-button {
367
- background: #444 !important;
368
- border-color: #555 !important;
369
- }
370
-
371
- /* Animation for page transitions */
372
- .fade-in {
373
- animation: fadeIn 0.5s ease-in;
374
- }
375
- @keyframes fadeIn {
376
- from { opacity: 0; }
377
- to { opacity: 1; }
378
- }
379
  """
380
 
381
- # ==================== BUILD MODERN UI ====================
382
 
383
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
384
- gr.Markdown(
385
- """<div style='text-align: center; padding: 20px 0;'>
386
- <h1 style='margin: 0; font-size: 2.5em;'>πŸŽ“ ThutoAI</h1>
387
- <p style='margin: 8px 0 0 0; font-size: 1.1em; color: #6c757d;'>Your Modern AI Learning Assistant</p>
388
- </div>"""
389
- )
390
 
391
- # Dark mode toggle (simulated with CSS class toggle)
392
  with gr.Row():
393
- dark_mode_btn = gr.Button("πŸŒ™ Toggle Dark Mode", variant="secondary")
394
- # Note: Full dark mode requires JS β€” we simulate with CSS classes
 
 
 
 
 
 
 
 
395
 
396
- with gr.Tab("πŸ“’ Announcements"):
397
- gr.Markdown("### πŸ“š Filter by Course or Subject")
 
398
  with gr.Row():
399
- course_filter = gr.Dropdown(
400
- choices=school_service.courses,
401
- value="All",
402
- label="Select Course",
403
- interactive=True
404
- )
405
- refresh_btn = gr.Button("πŸ”„ Refresh", variant="secondary")
406
- announcements_html = gr.HTML(elem_classes=["fade-in"])
407
- course_filter.change(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
408
- refresh_btn.click(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
409
- demo.load(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
410
-
411
- with gr.Tab("πŸ’¬ Ask ThutoAI"):
412
- gr.Markdown("### πŸ’‘ Ask me anything β€” I'm here to help!")
413
- chatbot = gr.Chatbot(
414
- height=480,
415
- bubble_full_width=False,
416
- elem_classes=["chatbot-container"]
417
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  with gr.Row():
419
- msg = gr.Textbox(
420
- label="Type your question",
421
- placeholder="E.g., How do I prepare for the Math exam?",
422
- scale=8
 
 
 
 
 
423
  )
424
- submit_btn = gr.Button("➀ Send", variant="primary", scale=1)
425
- clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", variant="secondary")
426
 
427
- def respond(message, chat_history):
428
- bot_reply = ai_chat(message, chat_history)
429
- chat_history.append((message, bot_reply))
430
- return "", chat_history
 
 
 
 
 
431
 
432
- msg.submit(respond, [msg, chatbot], [msg, chatbot])
433
- submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
434
- clear_btn.click(lambda: None, None, chatbot, queue=False)
 
 
435
 
436
- with gr.Tab("πŸ“‚ My Files"):
437
- gr.Markdown("### πŸ“ Upload & Manage Study Materials")
438
- with gr.Row():
439
- file_input = gr.File(label="Drag & drop or click to upload", elem_classes=["file-upload"])
440
- upload_btn = gr.Button("πŸ“€ Upload", variant="primary")
441
- status = gr.Textbox(label="Status", interactive=False)
442
- upload_btn.click(fn=school_service.upload_file, inputs=file_input, outputs=status)
443
-
444
- # File list preview (simulated)
445
- gr.Markdown("### πŸ“„ Recently Uploaded")
446
- file_list = gr.JSON(value=school_service.files[-5:] if school_service.files else [], label="Last 5 Files")
447
-
448
- with gr.Tab("πŸ” Teacher Admin"):
449
- gr.Markdown("### πŸ‘©β€πŸ« Post Announcements & View Stats")
450
-
451
- with gr.Group() as login_group:
452
- gr.Markdown("#### πŸ”‘ Teacher Login")
453
- with gr.Row():
454
- username = gr.Textbox(label="Username", placeholder="e.g., [email protected]", scale=3)
455
- password = gr.Textbox(label="Password", type="password", placeholder="β€’β€’β€’β€’β€’β€’β€’β€’", scale=3)
456
- login_btn = gr.Button("πŸ”“ Login", variant="primary")
457
- login_status = gr.Textbox(label="Status", interactive=False)
458
-
459
- with gr.Group(visible=False) as dashboard:
460
- gr.Markdown("#### πŸ“Š Dashboard Stats")
461
- stats = school_service.get_stats()
462
- gr.Markdown(f"""
463
- - πŸ“’ Total Announcements: **{stats['total_announcements']}**
464
- - πŸ“ Total Files Uploaded: **{stats['total_files']}**
465
- - 🎯 Active Courses: **{stats['active_courses']}**
466
- - 🚨 High Priority Posts: **{stats['high_priority']}**
467
- """)
468
-
469
- gr.Markdown("#### ✍️ Create New Announcement")
470
- with gr.Row():
471
- title = gr.Textbox(label="Title", placeholder="e.g., Quiz Moved to Friday", scale=3)
472
- course = gr.Dropdown(choices=school_service.courses[1:], label="Course", value="General", scale=2)
473
- content = gr.Textbox(label="Content", placeholder="Details for students...", lines=3)
474
- priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal", inline=True)
475
- post_btn = gr.Button("πŸ“¬ Post Announcement", variant="primary")
476
- post_result = gr.Textbox(label="Result", interactive=False)
477
-
478
- logout_btn = gr.Button("⬅️ Logout", variant="secondary")
479
-
480
- # Connect buttons
481
- login_btn.click(
482
- fn=admin_login,
483
- inputs=[username, password],
484
- outputs=[login_group, dashboard, login_status, post_btn]
485
- )
486
- logout_btn.click(
487
- fn=admin_logout,
488
- inputs=None,
489
- outputs=[login_group, dashboard, login_status, post_result]
490
- )
491
- post_btn.click(
492
- fn=post_announcement,
493
- inputs=[title, content, course, priority],
494
- outputs=post_result
495
- )
496
 
497
  # Launch app
498
  if __name__ == "__main__":
 
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
143
+ self.students = {
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."""
151
+ if not username or not password or not name:
152
+ return "⚠️ All fields required."
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."""
 
226
  },
227
  ]
228
  self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
 
229
  self.total_announcements = len(self.announcements)
230
  self.total_files = 0
231
 
 
247
  })
248
  self.total_announcements += 1
249
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  def get_school_context_for_ai(self) -> str:
251
  context = "Current School Announcements:\n"
252
+ for ann in self.announcements[-5:]:
253
  context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:80]}... (Priority: {ann['priority']})\n"
254
  return context
255
 
 
262
  }
263
 
264
 
265
+ # Initialize school service
266
+ school_service = SchoolService()
267
+
268
+ # ==================== ADMIN SERVICE ====================
269
+
270
  class AdminService:
271
  """Handles teacher authentication."""
272
 
 
280
  return self.admins.get(username) == password
281
 
282
 
 
 
283
  admin_service = AdminService()
284
 
285
+ # ==================== OPENAI SETUP ====================
286
+
287
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
288
  USE_OPENAI = bool(OPENAI_API_KEY)
289
 
 
297
  else:
298
  print("⚠️ OPENAI_API_KEY not set. Using mock responses.")
299
 
300
+ # ==================== AI CHAT FUNCTION ====================
301
 
302
+ def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
303
+ """Generate AI response with loading state and save to history."""
304
  if not message.strip():
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
 
312
  if USE_OPENAI:
313
  try:
 
322
  - Offer study tips if appropriate.
323
  - Never invent facts β€” say 'I don't know' if unsure."""
324
 
 
325
  response = client.chat.completions.create(
326
  model="gpt-3.5-turbo",
327
  messages=[
 
345
  emoji = "🚨" if ann["priority"] == "high" else "πŸ“Œ" if ann["priority"] == "normal" else "ℹ️"
346
  reply += f"\n{emoji} **{ann['title']}** ({ann['course']})\n β†’ {ann['content'][:70]}..."
347
 
 
 
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)
356
+
357
+ # Save to student's history if logged in
358
+ if username != "guest":
359
+ student_service.add_to_chat_history(username, message, reply)
360
 
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
 
 
391
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
392
  border-left: 4px solid {color};
393
  transition: transform 0.2s, box-shadow 0.2s;
394
+ '>
 
 
395
  <div style='display: flex; align-items: flex-start; gap: 12px;'>
396
  <div style='font-size: 1.5em;'>{icon}</div>
397
  <div style='flex: 1;'>
 
416
  </div>
417
  </div>
418
  """
 
419
  html += "</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
445
+ )
446
+ return (
447
+ gr.update(visible=True),
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 (
465
+ gr.update(visible=True), # Show login
466
+ gr.update(visible=False), # Hide main UI
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
523
 
524
  def admin_login(username: str, password: str) -> tuple:
 
525
  global IS_ADMIN
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),
533
+ stats_text,
534
  gr.update(visible=True)
535
  )
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
 
543
  def admin_logout():
 
544
  global IS_ADMIN
545
  IS_ADMIN = False
546
  return (
 
551
  )
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
 
566
  CUSTOM_CSS = """
 
567
  .gradio-container {
568
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
569
  max-width: 1200px;
570
  margin: 0 auto;
571
  padding: 16px;
572
  }
 
 
573
  .primary {
574
  background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
575
  border: none !important;
576
  color: white !important;
 
577
  }
 
 
 
 
 
 
578
  .chatbot-container {
579
  background: #f8f9fa !important;
580
  border-radius: 16px !important;
 
582
  .user, .bot {
583
  border-radius: 18px !important;
584
  padding: 12px 16px !important;
 
 
 
 
 
585
  }
 
 
586
  .file-upload {
587
  border: 2px dashed #6e8efb !important;
588
  border-radius: 12px !important;
589
  background: #f8f9ff !important;
590
  }
 
 
591
  .dark-mode {
592
  background: #1e1e1e !important;
593
  color: #f0f0f0 !important;
594
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  """
596
 
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")
631
+ reg_name = gr.Textbox(label="Full Name")
632
+ reg_btn = gr.Button("Create Account")
633
+ reg_status = gr.Textbox(label="Registration Status")
634
+
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!")
667
+ chatbot = gr.Chatbot(
668
+ height=480,
669
+ bubble_full_width=False,
670
+ elem_classes=["chatbot-container"]
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
+
686
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
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,
707
+ outputs=file_list
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,
733
+ inputs=[teacher_username, teacher_password],
734
+ outputs=[teacher_login_group, teacher_dashboard, teacher_status, post_btn]
735
+ )
736
+ teacher_logout_btn.click(
737
+ fn=admin_logout,
738
+ inputs=None,
739
+ outputs=[teacher_login_group, teacher_dashboard, teacher_status, post_result]
740
+ )
741
+ post_btn.click(
742
+ fn=post_announcement,
743
+ inputs=[ann_title, ann_content, ann_course, ann_priority],
744
+ outputs=post_result
745
+ )
746
+
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
  )
 
 
759
 
760
+ # ========= EVENT HANDLERS =========
761
+ login_btn.click(
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
+ )
769
 
770
+ register_btn.click(
771
+ fn=register_student,
772
+ inputs=[reg_username, reg_password, reg_name],
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__":