"""
ThutoAI - Complete School Assistant with Teacher Assignments & Analytics Dashboard
Meaning: "Thuto" = Learning/Education (Setswana โ used for branding only)
โ
Teacher Assignment Posting โ Assign work to class groups
โ
๐ Analytics Dashboard โ Track student engagement, assignments, groups
โ
๐ Dark Mode + ๐ผ๏ธ Profile Pictures
โ
๐๏ธ Voice Input + ๐
Assignment Tracker + ๐ฅ Class Groups
โ
All deprecation warnings fixed (Gradio 4.0+ compatible)
โ
Fully commented
"""
import os
import gradio as gr
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import time
import json
import base64
from collections import Counter
# ==================== STUDENT SERVICE ====================
class StudentService:
"""Manages student accounts, chat history, files, assignments, groups, and profile pictures."""
def __init__(self):
self.students = {
"student1": {"password": "pass123", "name": "John Doe", "avatar": None},
"student2": {"password": "pass456", "name": "Jane Smith", "avatar": None},
"student3": {"password": "pass789", "name": "Alex Johnson", "avatar": None}
}
self.student_sessions = {
"student1": {
"chat_history": [],
"files": [],
"assignments": [
{"id": 1, "title": "Math Quiz", "due_date": "2025-04-25", "course": "MATH10A", "status": "pending", "assigned_by": "Mr. Smith"},
{"id": 2, "title": "Science Lab Report", "due_date": "2025-04-30", "course": "SCI11B", "status": "pending", "assigned_by": "Dr. Lee"}
],
"groups": ["MATH10A", "SCI11B"],
"dark_mode": False
},
"student2": {
"chat_history": [],
"files": [],
"assignments": [
{"id": 3, "title": "History Essay", "due_date": "2025-04-22", "course": "HIST9A", "status": "overdue", "assigned_by": "Ms. Brown"},
{"id": 4, "title": "English Reading", "due_date": "2025-04-28", "course": "ENG10A", "status": "pending", "assigned_by": "Mr. White"}
],
"groups": ["HIST9A", "ENG10A"],
"dark_mode": True
},
"student3": {
"chat_history": [],
"files": [],
"assignments": [],
"groups": ["MATH10A", "ENG10A"],
"dark_mode": False
}
}
self.valid_groups = ["MATH10A", "MATH10B", "SCI11A", "SCI11B", "ENG10A", "ENG10B", "HIST9A", "HIST9B"]
self.all_assignments = [] # Master list for analytics
def register_student(self, username: str, password: str, name: str) -> str:
if not username or not password or not name:
return "โ ๏ธ All fields required."
if username in self.students:
return "โ ๏ธ Username already exists."
self.students[username] = {"password": password, "name": name, "avatar": None}
self.student_sessions[username] = {
"chat_history": [],
"files": [],
"assignments": [],
"groups": [],
"dark_mode": False
}
return "โ
Account created! Please log in."
def authenticate_student(self, username: str, password: str) -> Optional[Dict]:
student = self.students.get(username)
if student and student["password"] == password:
if username not in self.student_sessions:
self.student_sessions[username] = {
"chat_history": [],
"files": [],
"assignments": [],
"groups": [],
"dark_mode": False
}
return {
"name": student["name"],
"avatar": student["avatar"],
"dark_mode": self.student_sessions[username]["dark_mode"]
}
return None
def update_dark_mode(self, username: str, is_dark: bool):
if username in self.student_sessions:
self.student_sessions[username]["dark_mode"] = is_dark
def update_avatar(self, username: str, avatar_path: str):
if username in self.students and avatar_path:
self.students[username]["avatar"] = avatar_path
def get_chat_history(self, username: str) -> List:
return self.student_sessions.get(username, {}).get("chat_history", [])
def add_to_chat_history(self, username: str, user_msg: str, bot_reply: str):
if username in self.student_sessions:
self.student_sessions[username]["chat_history"].append((user_msg, bot_reply))
def get_files(self, username: str) -> List:
return self.student_sessions.get(username, {}).get("files", [])
def add_file(self, username: str, filename: str):
if username in self.student_sessions:
self.student_sessions[username]["files"].append({
"name": filename,
"uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
})
def get_assignments(self, username: str) -> List:
"""Get assignments for groups student has joined."""
if username not in self.student_sessions:
return []
student_groups = set(self.student_sessions[username]["groups"])
all_assignments = []
for user_data in self.student_sessions.values():
for assignment in user_data.get("assignments", []):
if assignment["course"] in student_groups:
all_assignments.append(assignment)
# Remove duplicates by ID
seen = set()
unique_assignments = []
for assignment in all_assignments:
if assignment["id"] not in seen:
seen.add(assignment["id"])
unique_assignments.append(assignment)
return sorted(unique_assignments, key=lambda x: x["due_date"])
def get_groups(self, username: str) -> List:
return self.student_sessions.get(username, {}).get("groups", [])
def join_group(self, username: str, group_code: str) -> str:
if group_code.upper() not in self.valid_groups:
return "โ Invalid group code. Ask your teacher for the correct code."
if username in self.student_sessions:
if group_code.upper() not in self.student_sessions[username]["groups"]:
self.student_sessions[username]["groups"].append(group_code.upper())
return f"โ
Joined group: {group_code.upper()}"
return "โ Login required."
def leave_group(self, username: str, group_code: str) -> str:
if username in self.student_sessions:
if group_code.upper() in self.student_sessions[username]["groups"]:
self.student_sessions[username]["groups"].remove(group_code.upper())
return f"โ
Left group: {group_code.upper()}"
return "โ Group not found or not joined."
# Analytics methods
def get_total_students(self) -> int:
return len(self.students)
def get_active_students(self) -> int:
return len([s for s in self.student_sessions.keys() if len(self.student_sessions[s]["assignments"]) > 0])
def get_total_assignments(self) -> int:
total = 0
for user_data in self.student_sessions.values():
total += len(user_data.get("assignments", []))
return total
def get_completed_assignments(self) -> int:
completed = 0
for user_data in self.student_sessions.values():
for assignment in user_data.get("assignments", []):
if assignment["status"] == "completed":
completed += 1
return completed
def get_group_popularity(self) -> Dict:
all_groups = []
for user_data in self.student_sessions.values():
all_groups.extend(user_data.get("groups", []))
return dict(Counter(all_groups))
def get_recent_activity(self) -> List:
activity = []
for username, data in self.student_sessions.items():
if data["assignments"]:
latest = max(data["assignments"], key=lambda x: x.get("assigned_date", "2025-01-01"))
activity.append({
"student": self.students[username]["name"],
"last_assignment": latest["title"],
"date": latest.get("assigned_date", "Unknown")
})
return sorted(activity, key=lambda x: x["date"], reverse=True)[:5]
# Initialize student service
student_service = StudentService()
# ==================== SCHOOL SERVICE ====================
class SchoolService:
"""Handles announcements, AI context, and shared assignments."""
def __init__(self):
self.announcements = [
{
"id": 1,
"title": "Math Final Exam",
"content": "Covers chapters 1-5. Bring calculator and ruler.",
"course": "Mathematics",
"date": "2025-04-15",
"priority": "high",
"posted_by": "Mr. Smith",
"views": 45
},
{
"id": 2,
"title": "Science Project Due",
"content": "Submit your ecology report by Friday. Include bibliography.",
"course": "Science",
"date": "2025-04-12",
"priority": "normal",
"posted_by": "Dr. Lee",
"views": 32
},
{
"id": 3,
"title": "Library Extended Hours",
"content": "Open until 9 PM during exam week. Quiet study zones available.",
"course": "General",
"date": "2025-04-10",
"priority": "low",
"posted_by": "Librarian",
"views": 67
},
]
self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
self.total_announcements = len(self.announcements)
self.total_files = 0
self.assignment_id_counter = 5 # Start after existing assignments
def get_announcements(self, course_filter: str = "All") -> List[Dict]:
if course_filter == "All":
return self.announcements
return [a for a in self.announcements if a["course"] == course_filter]
def add_announcement(self, title: str, content: str, course: str, priority: str, posted_by: str = "Admin"):
new_id = max([a["id"] for a in self.announcements], default=0) + 1
self.announcements.append({
"id": new_id,
"title": title,
"content": content,
"course": course,
"date": datetime.now().strftime("%Y-%m-%d"),
"priority": priority,
"posted_by": posted_by,
"views": 0
})
self.total_announcements += 1
def get_school_context_for_ai(self) -> str:
context = "Current School Announcements:\n"
for ann in self.announcements[-5:]:
context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:80]}... (Priority: {ann['priority']})\n"
return context
def get_stats(self) -> Dict:
return {
"total_announcements": self.total_announcements,
"total_files": self.total_files,
"active_courses": len(set(a["course"] for a in self.announcements if a["course"] != "General")),
"high_priority": len([a for a in self.announcements if a["priority"] == "high"]),
"total_views": sum(a["views"] for a in self.announcements)
}
def create_assignment(self, title: str, description: str, due_date: str, course: str, assigned_by: str) -> int:
"""Create assignment for a specific class group."""
assignment_id = self.assignment_id_counter
self.assignment_id_counter += 1
new_assignment = {
"id": assignment_id,
"title": title,
"description": description,
"due_date": due_date,
"course": course,
"assigned_by": assigned_by,
"assigned_date": datetime.now().strftime("%Y-%m-%d"),
"status": "pending"
}
# Assign to all students in this group
for username, data in student_service.student_sessions.items():
if course in data["groups"]:
data["assignments"].append(new_assignment.copy())
return assignment_id
school_service = SchoolService()
# ==================== ADMIN SERVICE ====================
class AdminService:
"""Handles teacher and admin authentication."""
def __init__(self):
self.teachers = {
"teacher@thutoai.edu": {"password": "password123", "name": "Mr. Smith", "role": "teacher"},
"scienceteacher@school.org": {"password": "science123", "name": "Dr. Lee", "role": "teacher"},
"admin@school.org": {"password": "letmein", "name": "Admin", "role": "admin"}
}
def authenticate(self, username: str, password: str) -> Optional[Dict]:
user = self.teachers.get(username)
if user and user["password"] == password:
return {"name": user["name"], "role": user["role"]}
return None
admin_service = AdminService()
# ==================== OPENAI SETUP ====================
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
USE_OPENAI = bool(OPENAI_API_KEY)
if USE_OPENAI:
try:
from openai import OpenAI
client = OpenAI(api_key=OPENAI_API_KEY)
except Exception as e:
print(f"โ ๏ธ OpenAI error: {e}")
USE_OPENAI = False
else:
print("โ ๏ธ OPENAI_API_KEY not set. Using mock responses.")
# ==================== AI CHAT FUNCTION ====================
def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
"""Generate AI response with loading state and save to history."""
if not message.strip():
return history, ""
thinking_msg = "๐ค ThutoAI is thinking..."
history.append((message, thinking_msg))
yield history, ""
if USE_OPENAI:
try:
system_prompt = f"""You are ThutoAI, a friendly and knowledgeable AI assistant for students.
Context from school:
{school_service.get_school_context_for_ai()}
Guidelines:
- Be encouraging, clear, and concise.
- Use emojis sparingly to keep it fun ๐๐๐ฏ
- If asked about deadlines or exams, refer to context.
- Offer study tips if appropriate.
- Never invent facts โ say 'I don't know' if unsure."""
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": message}
],
temperature=0.7,
max_tokens=500
)
reply = response.choices[0].message.content.strip()
keywords = ["exam", "test", "due", "assignment", "deadline", "when", "what", "grade", "score"]
if any(kw in message.lower() for kw in keywords):
matches = school_service.get_announcements()
relevant = [a for a in matches if any(kw in a["title"].lower() or kw in a["content"].lower() for kw in keywords)]
if relevant:
reply += "\n\n๐ **Quick Info from School:**"
for ann in relevant[:2]:
emoji = "๐จ" if ann["priority"] == "high" else "๐" if ann["priority"] == "normal" else "โน๏ธ"
reply += f"\n{emoji} **{ann['title']}** ({ann['course']})\n โ {ann['content'][:70]}..."
except Exception as e:
reply = f"โ ๏ธ Sorry, I had a glitch: {str(e)}"
else:
time.sleep(1.5)
reply = f"๐ Hi! I'm ThutoAI. You asked: '{message}'.\n๐ก *Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!*"
history[-1] = (message, reply)
if username != "guest":
student_service.add_to_chat_history(username, message, reply)
yield history, ""
# ==================== UI RENDERING HELPERS ====================
def render_announcements(course: str) -> str:
"""Render announcements with modern cards."""
announcements = school_service.get_announcements(course)
if not announcements:
return """
๐ญ
No announcements for this course.
Check back later or select "All" to see everything.
"""
html = ""
priority_icons = {"high": "๐จ", "normal": "๐", "low": "โน๏ธ"}
priority_colors = {"high": "#dc3545", "normal": "#ffc107", "low": "#6c757d"}
for ann in announcements:
icon = priority_icons.get(ann["priority"], "๐")
color = priority_colors.get(ann["priority"], "#6c757d")
html += f"""
{icon}
{ann['title']}
{ann['priority'].upper()}
{ann['content']}
๐ {ann['course']}
๐
{ann['date']}
๐จโ๐ซ {ann['posted_by']}
๐๏ธ {ann['views']} views
"""
html += "
"
return html
def render_assignments(assignments: List[Dict]) -> str:
"""Render assignments in a clean, prioritized list."""
if not assignments:
return """
๐
No upcoming assignments.
Ask your teacher or join a class group to see assignments.
"""
html = ""
today = datetime.today().date()
for task in assignments:
due_date = datetime.strptime(task["due_date"], "%Y-%m-%d").date()
days_left = (due_date - today).days
is_overdue = days_left < 0
is_today = days_left == 0
if is_overdue:
badge = "๐จ OVERDUE"
color = "#dc3545"
elif is_today:
badge = "๐ฏ TODAY"
color = "#fd7e14"
elif days_left <= 2:
badge = f"โ ๏ธ Due in {days_left} day{'s' if days_left != 1 else ''}"
color = "#ffc107"
else:
badge = f"โ
Due in {days_left} days"
color = "#28a745"
html += f"""
{task['title']}
๐ {task['course']} ยท ๐ฉโ๐ซ {task['assigned_by']}
๐
Due: {task['due_date']}
{task.get('description', '')}
{badge}
"""
html += "
"
return html
def render_groups(groups: List[str]) -> str:
"""Render joined class groups."""
if not groups:
return """
๐ฅ
You haven't joined any class groups yet.
Ask your teacher for a group code to join your class.
"""
html = ""
for group in groups:
html += f"""
"""
html += "
"
return html
def get_avatar_html(avatar_path: Optional[str], name: str) -> str:
"""Generate HTML for avatar display."""
if avatar_path:
try:
with open(avatar_path, "rb") as f:
img_data = base64.b64encode(f.read()).decode()
img_html = f'
'
except:
img_html = f'{name[0].upper()}
'
else:
img_html = f'{name[0].upper()}
'
return img_html
def render_analytics() -> str:
"""Render analytics dashboard for admins."""
total_students = student_service.get_total_students()
active_students = student_service.get_active_students()
total_assignments = student_service.get_total_assignments()
completed_assignments = student_service.get_completed_assignments()
group_popularity = student_service.get_group_popularity()
recent_activity = student_service.get_recent_activity()
school_stats = school_service.get_stats()
completion_rate = (completed_assignments / total_assignments * 100) if total_assignments > 0 else 0
html = f"""
๐ฅ Student Engagement
{total_students}
Total Students
{active_students}
Active Students
๐ Assignment Stats
{total_assignments}
Total Assignments
{completion_rate:.1f}%
Completion Rate
๐ Popular Class Groups
"""
sorted_groups = sorted(group_popularity.items(), key=lambda x: x[1], reverse=True)
for group, count in sorted_groups[:5]:
percentage = (count / total_students * 100) if total_students > 0 else 0
html += f"""
"""
html += """
๐ข Announcement Stats
"""
for ann in school_service.announcements[:5]:
views = ann.get("views", 0)
max_views = max(a.get("views", 0) for a in school_service.announcements) if school_service.announcements else 1
width_percent = (views / max_views * 100) if max_views > 0 else 0
html += f"""
{ann['title'][:20]}...
{views} views
"""
html += """
โฑ๏ธ Recent Activity
"""
for activity in recent_activity:
html += f"""
{activity['student']} received assignment: {activity['last_assignment']}
{activity['date']}
"""
html += """
"""
return html
# ==================== STATE MANAGEMENT ====================
CURRENT_USER = "guest"
CURRENT_TEACHER = None
DARK_MODE = False
def login_student(username: str, password: str) -> tuple:
global CURRENT_USER, DARK_MODE
student_data = student_service.authenticate_student(username, password)
if student_data:
CURRENT_USER = username
DARK_MODE = student_data["dark_mode"]
chat_history = student_service.get_chat_history(username)
files = student_service.get_files(username)
assignments = student_service.get_assignments(username)
groups = student_service.get_groups(username)
avatar_html = get_avatar_html(student_data["avatar"], student_data["name"])
welcome_msg = f"Welcome back, {student_data['name']}!"
css_class = "dark-mode" if DARK_MODE else ""
return (
gr.update(visible=False),
gr.update(visible=True),
chat_history,
files,
render_assignments(assignments),
render_groups(groups),
welcome_msg,
gr.update(value=student_data["name"], visible=True),
gr.update(visible=True),
gr.update(value=avatar_html),
gr.update(value="๐ Light Mode" if DARK_MODE else "โ๏ธ Dark Mode"),
css_class
)
return (
gr.update(visible=True),
gr.update(visible=False),
[],
[],
"",
"",
"โ Invalid username or password",
gr.update(visible=False),
gr.update(visible=False),
gr.update(),
gr.update(),
""
)
def register_student(username: str, password: str, name: str) -> str:
return student_service.register_student(username, password, name)
def logout_student() -> tuple:
global CURRENT_USER, DARK_MODE
CURRENT_USER = "guest"
DARK_MODE = False
return (
gr.update(visible=True),
gr.update(visible=False),
[],
[],
"",
"",
"",
gr.update(visible=False),
gr.update(visible=False),
gr.update(),
gr.update(value="โ๏ธ Dark Mode"),
""
)
def toggle_dark_mode() -> tuple:
global DARK_MODE, CURRENT_USER
DARK_MODE = not DARK_MODE
if CURRENT_USER != "guest":
student_service.update_dark_mode(CURRENT_USER, DARK_MODE)
btn_text = "๐ Light Mode" if DARK_MODE else "โ๏ธ Dark Mode"
css_class = "dark-mode" if DARK_MODE else ""
return btn_text, css_class
def upload_avatar(file) -> str:
if not file:
return "โ No file selected"
if CURRENT_USER == "guest":
return "โ Please log in first"
student_service.update_avatar(CURRENT_USER, file.name)
return f"โ
Avatar updated!"
def join_group(group_code: str) -> tuple:
if CURRENT_USER == "guest":
return "โ Please log in first.", ""
result = student_service.join_group(CURRENT_USER, group_code)
groups = student_service.get_groups(CURRENT_USER)
return result, render_groups(groups)
def leave_group(group_code: str) -> tuple:
if CURRENT_USER == "guest":
return "โ Please log in first.", ""
result = student_service.leave_group(CURRENT_USER, group_code)
groups = student_service.get_groups(CURRENT_USER)
return result, render_groups(groups)
def upload_file_for_student(file) -> str:
if not file:
return "โ No file selected"
result = f"โ
Uploaded: {file.name}"
if CURRENT_USER != "guest":
student_service.add_file(CURRENT_USER, file.name)
return result
# ==================== TEACHER FUNCTIONS ====================
def teacher_login(username: str, password: str) -> tuple:
global CURRENT_TEACHER
teacher_data = admin_service.authenticate(username, password)
if teacher_data and teacher_data["role"] == "teacher":
CURRENT_TEACHER = teacher_data
return (
gr.update(visible=False),
gr.update(visible=True),
f"โ
Welcome, {teacher_data['name']}!",
gr.update(visible=True)
)
elif teacher_data and teacher_data["role"] == "admin":
CURRENT_TEACHER = teacher_data
return (
gr.update(visible=False),
gr.update(visible=True),
f"โ
Welcome, Admin {teacher_data['name']}!",
gr.update(visible=True)
)
return (
gr.update(visible=True),
gr.update(visible=False),
"โ Invalid credentials or not a teacher account",
gr.update(visible=False)
)
def teacher_logout():
global CURRENT_TEACHER
CURRENT_TEACHER = None
return (
gr.update(visible=True),
gr.update(visible=False),
"",
gr.update(visible=False)
)
def create_assignment(title: str, description: str, due_date: str, course: str) -> str:
if not CURRENT_TEACHER:
return "๐ Please log in as teacher first."
if not title or not due_date or not course:
return "โ ๏ธ Title, due date, and course are required."
if course not in student_service.valid_groups:
return "โ ๏ธ Invalid course/group. Choose from available groups."
assignment_id = school_service.create_assignment(
title=title,
description=description,
due_date=due_date,
course=course,
assigned_by=CURRENT_TEACHER["name"]
)
# Count how many students received this assignment
recipients = 0
for data in student_service.student_sessions.values():
if course in data["groups"]:
recipients += 1
return f"โ
Assignment created! ID: {assignment_id}. Sent to {recipients} students in {course}."
# ==================== VOICE INPUT ====================
VOICE_JS = """
async function startVoiceInput() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('๐๏ธ Voice input is not supported in your browser. Try Chrome or Edge.');
return '';
}
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;
return new Promise((resolve) => {
recognition.start();
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
resolve(transcript);
};
recognition.onerror = (event) => {
alert('๐๏ธ Error: ' + event.error);
resolve('');
};
});
}
"""
def voice_input_handler() -> str:
return ""
# ==================== CUSTOM CSS ====================
CUSTOM_CSS = """
.gradio-container {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
transition: background-color 0.3s, color 0.3s;
}
.dark-mode {
--background-fill-primary: #1e1e1e !important;
--background-fill-secondary: #2d2d2d !important;
--text-color: #f0f0f0 !important;
--button-primary-background-fill: #5a5a5a !important;
--button-secondary-background-fill: #3a3a3a !important;
--input-background-fill: #2d2d2d !important;
--input-border-color: #444 !important;
}
.primary {
background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
border: none !important;
color: white !important;
}
.chatbot-container {
background: #f8f9fa !important;
border-radius: 16px !important;
}
.dark-mode .chatbot-container {
background: #2d2d2d !important;
}
.user, .bot {
border-radius: 18px !important;
padding: 12px 16px !important;
}
.file-upload {
border: 2px dashed #6e8efb !important;
border-radius: 12px !important;
background: #f8f9ff !important;
}
.dark-mode .file-upload {
background: #2a2a2a !important;
border-color: #5a5a5a !important;
}
.analytics-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
"""
# ==================== BUILD UI ====================
with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
# Header with dark mode toggle
with gr.Row():
gr.Markdown("# ๐ ThutoAI โ Your AI School Assistant")
dark_mode_btn = gr.Button("โ๏ธ Dark Mode", variant="secondary")
# ========= STUDENT LOGIN/REGISTER =========
with gr.Group() as login_group:
gr.Markdown("### ๐ Student Login")
with gr.Row():
login_username = gr.Textbox(label="Username", scale=3)
login_password = gr.Textbox(label="Password", type="password", scale=3)
login_btn = gr.Button("๐ Login", variant="primary")
register_btn = gr.Button("๐ Register", variant="secondary")
login_status = gr.Textbox(label="Status", interactive=False)
with gr.Accordion("๐ Register New Account", open=False):
reg_username = gr.Textbox(label="Username")
reg_password = gr.Textbox(label="Password", type="password")
reg_name = gr.Textbox(label="Full Name")
reg_btn = gr.Button("Create Account")
reg_status = gr.Textbox(label="Registration Status")
# ========= MAIN APP (hidden until login) =========
with gr.Group(visible=False) as main_app:
# Profile header
with gr.Row():
avatar_display = gr.HTML()
user_display = gr.Textbox(label="Logged in as", interactive=False, visible=True)
logout_btn = gr.Button("โฌ
๏ธ Logout", variant="secondary")
with gr.Tabs():
with gr.Tab("๐ข Announcements"):
gr.Markdown("### Filter by Course or Subject")
with gr.Row():
course_filter = gr.Dropdown(
choices=school_service.courses,
value="All",
label="Select Course",
scale=3
)
refresh_btn = gr.Button("๐ Refresh", variant="secondary", scale=1)
announcements_html = gr.HTML()
course_filter.change(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
refresh_btn.click(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
demo.load(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
with gr.Tab("๐ฌ Ask ThutoAI"):
gr.Markdown("### ๐ก Ask me anything โ I'm here to help!")
chatbot = gr.Chatbot(
height=480,
type='messages' # โ
FIXED: Replaced deprecated bubble_full_width
)
with gr.Row():
msg = gr.Textbox(
label="Type your question",
placeholder="E.g., How do I prepare for the Math exam?",
scale=7
)
voice_btn = gr.Button("๐๏ธ Speak", variant="secondary", scale=1)
submit_btn = gr.Button("โค Send", variant="primary", scale=1)
clear_btn = gr.Button("๐๏ธ Clear Chat", variant="secondary")
def respond(message, chat_history):
for updated_history, _ in ai_chat(message, chat_history, CURRENT_USER):
yield updated_history, ""
msg.submit(respond, [msg, chatbot], [msg, chatbot])
submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
clear_btn.click(lambda: [], None, chatbot)
voice_btn.click(
fn=voice_input_handler,
inputs=None,
outputs=msg,
js=VOICE_JS + "return startVoiceInput();"
)
with gr.Tab("๐
Assignments"):
gr.Markdown("### ๐ Your Upcoming Assignments & Exams")
assignments_display = gr.HTML()
demo.load(
fn=lambda: render_assignments(student_service.get_assignments(CURRENT_USER)) if CURRENT_USER != "guest" else "",
inputs=None,
outputs=assignments_display
)
with gr.Tab("๐ฅ Class Groups"):
gr.Markdown("### ๐ Join Your Class Groups")
with gr.Row():
group_code_input = gr.Textbox(label="Group Code (e.g., MATH10A)", scale=3)
join_btn = gr.Button("โ Join Group", variant="primary", scale=1)
leave_btn = gr.Button("โ Leave Group", variant="secondary", scale=1)
group_status = gr.Textbox(label="Status")
groups_display = gr.HTML()
join_btn.click(
fn=join_group,
inputs=group_code_input,
outputs=[group_status, groups_display]
)
leave_btn.click(
fn=leave_group,
inputs=group_code_input,
outputs=[group_status, groups_display]
)
demo.load(
fn=lambda: render_groups(student_service.get_groups(CURRENT_USER)) if CURRENT_USER != "guest" else "",
inputs=None,
outputs=groups_display
)
with gr.Tab("๐ My Files"):
gr.Markdown("### ๐ Upload & Manage Study Materials")
with gr.Row():
file_input = gr.File(label="Drag & drop or click to upload", elem_classes=["file-upload"])
upload_btn = gr.Button("๐ค Upload", variant="primary")
upload_status = gr.Textbox(label="Status")
file_list = gr.JSON(label="Your Files")
upload_btn.click(
fn=upload_file_for_student,
inputs=file_input,
outputs=upload_status
)
demo.load(
fn=lambda: student_service.get_files(CURRENT_USER) if CURRENT_USER != "guest" else [],
inputs=None,
outputs=file_list
)
with gr.Tab("๐ผ๏ธ Profile"):
gr.Markdown("### ๐ผ๏ธ Update Your Profile Picture")
with gr.Row():
avatar_input = gr.File(label="Choose an image (PNG, JPG)", file_types=["image"])
upload_avatar_btn = gr.Button("๐ค Upload Avatar", variant="primary")
avatar_status = gr.Textbox(label="Status")
upload_avatar_btn.click(
fn=upload_avatar,
inputs=avatar_input,
outputs=avatar_status
)
demo.load(
fn=lambda: get_avatar_html(
student_service.students[CURRENT_USER]["avatar"] if CURRENT_USER != "guest" else None,
student_service.students[CURRENT_USER]["name"] if CURRENT_USER != "guest" else "Guest"
) if CURRENT_USER != "guest" else "",
inputs=None,
outputs=avatar_display
)
with gr.Tab("๐ฉโ๐ซ Teacher Panel"):
gr.Markdown("### ๐ Teacher Login (for assignment creation)")
with gr.Group() as teacher_login_group:
teacher_username = gr.Textbox(label="Teacher Username")
teacher_password = gr.Textbox(label="Password", type="password")
teacher_login_btn = gr.Button("๐ Login", variant="primary")
teacher_status = gr.Textbox(label="Status")
with gr.Group(visible=False) as teacher_dashboard:
gr.Markdown("### โ๏ธ Create New Assignment")
assignment_title = gr.Textbox(label="Assignment Title", placeholder="e.g., Chapter 5 Quiz")
assignment_description = gr.Textbox(label="Description", placeholder="Instructions for students...", lines=2)
assignment_due_date = gr.Textbox(label="Due Date (YYYY-MM-DD)", placeholder="2025-05-01")
assignment_course = gr.Dropdown(
choices=student_service.valid_groups,
label="Assign to Class Group",
value="MATH10A"
)
create_assignment_btn = gr.Button("๐ฌ Create Assignment", variant="primary")
assignment_result = gr.Textbox(label="Result")
# โ
FIXED: Removed deprecated 'inline' parameter
assignment_priority = gr.Radio(
["low", "normal", "high"],
label="Priority",
value="normal"
)
teacher_logout_btn = gr.Button("โฌ
๏ธ Logout", variant="secondary")
teacher_login_btn.click(
fn=teacher_login,
inputs=[teacher_username, teacher_password],
outputs=[teacher_login_group, teacher_dashboard, teacher_status, create_assignment_btn]
)
teacher_logout_btn.click(
fn=teacher_logout,
inputs=None,
outputs=[teacher_login_group, teacher_dashboard, teacher_status, assignment_result]
)
create_assignment_btn.click(
fn=create_assignment,
inputs=[assignment_title, assignment_description, assignment_due_date, assignment_course],
outputs=assignment_result
)
with gr.Tab("๐ Admin Analytics"):
gr.Markdown("### ๐ School Analytics Dashboard")
analytics_display = gr.HTML()
refresh_analytics_btn = gr.Button("๐ Refresh Analytics")
refresh_analytics_btn.click(
fn=render_analytics,
inputs=None,
outputs=analytics_display
)
demo.load(
fn=render_analytics,
inputs=None,
outputs=analytics_display
)
# Logout button (already in header)
# ========= EVENT HANDLERS =========
login_btn.click(
fn=login_student,
inputs=[login_username, login_password],
outputs=[
login_group, main_app, chatbot, gr.update(), assignments_display, groups_display,
login_status, user_display, logout_btn, avatar_display, dark_mode_btn, gr.update()
]
)
register_btn.click(
fn=register_student,
inputs=[reg_username, reg_password, reg_name],
outputs=reg_status
)
dark_mode_btn.click(
fn=toggle_dark_mode,
inputs=None,
outputs=[dark_mode_btn, gr.update()]
)
# Launch app
if __name__ == "__main__":
demo.launch()