SmartMate / app.py
ngwakomadikwe's picture
Update app.py
89144ca verified
raw
history blame
48.3 kB
"""
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 = {
"[email protected]": {"password": "password123", "name": "Mr. Smith", "role": "teacher"},
"[email protected]": {"password": "science123", "name": "Dr. Lee", "role": "teacher"},
"[email protected]": {"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 """
<div style='text-align: center; padding: 40px; color: #6c757d;'>
<div style='font-size: 4em; margin-bottom: 16px;'>πŸ“­</div>
<h3>No announcements for this course.</h3>
<p>Check back later or select "All" to see everything.</p>
</div>
"""
html = "<div style='display: grid; gap: 16px;'>"
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"""
<div style='
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-left: 4px solid {color};
transition: transform 0.2s, box-shadow 0.2s;
'>
<div style='display: flex; align-items: flex-start; gap: 12px;'>
<div style='font-size: 1.5em;'>{icon}</div>
<div style='flex: 1;'>
<div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'>
<h3 style='margin: 0; color: #212529;'>{ann['title']}</h3>
<span style='
background: {color};
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8em;
font-weight: bold;
'>{ann['priority'].upper()}</span>
</div>
<p style='margin: 8px 0; color: #495057; line-height: 1.5;'>{ann['content']}</p>
<div style='display: flex; justify-content: space-between; font-size: 0.85em; color: #6c757d; margin-top: 12px;'>
<span>πŸ“š {ann['course']}</span>
<span>πŸ“… {ann['date']}</span>
<span>πŸ‘¨β€πŸ« {ann['posted_by']}</span>
<span>πŸ‘οΈ {ann['views']} views</span>
</div>
</div>
</div>
</div>
"""
html += "</div>"
return html
def render_assignments(assignments: List[Dict]) -> str:
"""Render assignments in a clean, prioritized list."""
if not assignments:
return """
<div style='text-align: center; padding: 40px; color: #6c757d;'>
<div style='font-size: 4em; margin-bottom: 16px;'>πŸ“…</div>
<h3>No upcoming assignments.</h3>
<p>Ask your teacher or join a class group to see assignments.</p>
</div>
"""
html = "<div style='display: grid; gap: 16px;'>"
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"""
<div style='
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-left: 4px solid {color};
transition: transform 0.2s;
'>
<div style='display: flex; justify-content: space-between; align-items: flex-start;'>
<div>
<h3 style='margin: 0 0 8px 0; color: #212529;'>{task['title']}</h3>
<div style='color: #6c757d; margin-bottom: 8px;'>πŸ“š {task['course']} Β· πŸ‘©β€πŸ« {task['assigned_by']}</div>
<div style='color: #495057;'>πŸ“… Due: {task['due_date']}</div>
<div style='color: #6c757d; font-size: 0.9em;'>{task.get('description', '')}</div>
</div>
<span style='
background: {color};
color: white;
padding: 6px 12px;
border-radius: 20px;
font-weight: bold;
font-size: 0.85em;
align-self: flex-start;
'>{badge}</span>
</div>
</div>
"""
html += "</div>"
return html
def render_groups(groups: List[str]) -> str:
"""Render joined class groups."""
if not groups:
return """
<div style='text-align: center; padding: 40px; color: #6c757d;'>
<div style='font-size: 4em; margin-bottom: 16px;'>πŸ‘₯</div>
<h3>You haven't joined any class groups yet.</h3>
<p>Ask your teacher for a group code to join your class.</p>
</div>
"""
html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;'>"
for group in groups:
html += f"""
<div style='
background: #e3f2fd;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: 2px solid #2196f3;
'>
<div style='font-size: 1.5em; margin-bottom: 8px;'>πŸŽ’</div>
<h3 style='margin: 0; color: #1976d2;'>{group}</h3>
<p style='margin: 8px 0 0 0; color: #555; font-size: 0.9em;'>Class Group</p>
</div>
"""
html += "</div>"
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'<img src="data:image/png;base64,{img_data}" style="width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 2px solid #6e8efb;">'
except:
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>'
else:
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>'
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"""
<div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; margin-bottom: 32px;'>
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ‘₯ Student Engagement</h3>
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
<div style='text-align: center; padding: 16px; background: #e3f2fd; border-radius: 8px;'>
<div style='font-size: 2em; color: #1976d2;'>{total_students}</div>
<div>Total Students</div>
</div>
<div style='text-align: center; padding: 16px; background: #e8f5e8; border-radius: 8px;'>
<div style='font-size: 2em; color: #2e7d32;'>{active_students}</div>
<div>Active Students</div>
</div>
</div>
</div>
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“Š Assignment Stats</h3>
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
<div style='text-align: center; padding: 16px; background: #fff3e0; border-radius: 8px;'>
<div style='font-size: 2em; color: #ef6c00;'>{total_assignments}</div>
<div>Total Assignments</div>
</div>
<div style='text-align: center; padding: 16px; background: #f3e5f5; border-radius: 8px;'>
<div style='font-size: 2em; color: #7b1fa2;'>{completion_rate:.1f}%</div>
<div>Completion Rate</div>
</div>
</div>
</div>
</div>
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 32px;'>
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“ˆ Popular Class Groups</h3>
<div style='display: grid; gap: 12px;'>
"""
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"""
<div style='display: flex; align-items: center; gap: 12px;'>
<div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
<div style='width: {percentage}%; background: #6e8efb; height: 100%; border-radius: 4px;'></div>
</div>
<div style='min-width: 80px;'>{group}</div>
<div style='color: #6c757d;'>{count} students</div>
</div>
"""
html += """
</div>
</div>
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<h3 style='color: #212529; margin: 0 0 16px 0;'>πŸ“’ Announcement Stats</h3>
<div style='display: grid; gap: 12px;'>
"""
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"""
<div style='display: flex; align-items: center; gap: 12px;'>
<div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
<div style='width: {width_percent}%; background: #ff7043; height: 100%; border-radius: 4px;'></div>
</div>
<div style='min-width: 120px; font-size: 0.9em;'>{ann['title'][:20]}...</div>
<div style='color: #6c757d;'>{views} views</div>
</div>
"""
html += """
</div>
</div>
</div>
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<h3 style='color: #212529; margin: 0 0 16px 0;'>⏱️ Recent Activity</h3>
<div style='display: grid; gap: 12px;'>
"""
for activity in recent_activity:
html += f"""
<div style='padding: 12px; background: #f8f9fa; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;'>
<div>
<strong>{activity['student']}</strong> received assignment: <em>{activity['last_assignment']}</em>
</div>
<div style='color: #6c757d; font-size: 0.9em;'>{activity['date']}</div>
</div>
"""
html += """
</div>
</div>
"""
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()