Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
| 1 |
"""
|
| 2 |
-
ThutoAI - Complete School Assistant with
|
| 3 |
Meaning: "Thuto" = Learning/Education (Setswana β used for branding only)
|
| 4 |
|
| 5 |
-
β
Student Accounts
|
| 6 |
-
β
|
| 7 |
-
β
|
| 8 |
-
β
|
| 9 |
-
β
|
| 10 |
-
β
|
|
|
|
| 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
|
| 20 |
|
| 21 |
-
# ==================== STUDENT
|
| 22 |
|
| 23 |
class StudentService:
|
| 24 |
-
"""Manages student accounts, chat history, files, assignments, and
|
| 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[
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 471 |
-
|
| 472 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
return (
|
| 481 |
-
gr.update(visible=False),
|
| 482 |
-
gr.update(visible=True),
|
| 483 |
chat_history,
|
| 484 |
files,
|
| 485 |
render_assignments(assignments),
|
| 486 |
render_groups(groups),
|
| 487 |
welcome_msg,
|
| 488 |
-
gr.update(value=
|
| 489 |
-
gr.update(visible=True)
|
|
|
|
|
|
|
|
|
|
| 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),
|
| 511 |
-
gr.update(visible=False),
|
| 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
|
| 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 |
-
|
| 647 |
-
|
| 648 |
-
|
|
|
|
| 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.
|
| 657 |
-
|
| 658 |
-
|
| 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()
|