""" 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"""
๐ŸŽ’

{group}

Class Group

""" 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"""
{group}
{count} students
""" 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()