SmartMate / app.py
ngwakomadikwe's picture
Update app.py
ed2729e verified
raw
history blame
12.3 kB
"""
ThutoAI - Complete Student Assistant with School Management
Refactored for Hugging Face Spaces using Gradio (Flask won't work here)
Enhanced with mobile UI, course filtering, admin panel, and smarter AI chat.
"""
import os
import json
from datetime import datetime
from typing import List, Dict, Any, Optional
import gradio as gr
# ==================== MOCK SERVICES (Replace with real DB logic) ====================
class MockSchoolService:
"""Simulates school data services. Replace with real DB/API calls."""
def __init__(self):
# Sample data β€” replace with database queries in production
self.announcements = [
{"id": 1, "title": "Math Exam Reminder", "content": "Final exam next Monday. Chapters 1-5.", "course": "Mathematics", "date": "2025-04-10", "priority": "high"},
{"id": 2, "title": "Science Fair", "content": "Submit projects by Friday.", "course": "Science", "date": "2025-04-08", "priority": "normal"},
{"id": 3, "title": "Library Hours Update", "content": "Open until 8 PM during exams.", "course": "General", "date": "2025-04-07", "priority": "low"},
]
self.courses = ["Mathematics", "Science", "English", "History", "General"]
self.files = [] # Simulate uploaded files
def get_announcements_by_course(self, course: str = "All") -> List[Dict]:
if course == "All":
return self.announcements
return [a for a in self.announcements if a["course"] == course]
def add_announcement(self, title: str, content: str, course: str, priority: str):
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
})
def upload_file(self, file):
if file:
self.files.append({"name": file.name, "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")})
return f"βœ… Uploaded: {file.name}" if file else "❌ No file selected"
def search_school_info(self, query: str) -> List[Dict]:
results = []
for ann in self.announcements:
if query.lower() in ann["title"].lower() or query.lower() in ann["content"].lower():
results.append(ann)
return results
def format_school_context_for_ai(self) -> str:
context = "Current School Context:\n"
for ann in self.announcements[:5]: # Top 5 recent
context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:50]}... (Priority: {ann['priority']})\n"
return context
class MockAdminService:
"""Simulates admin authentication and management."""
def __init__(self):
self.admins = {"[email protected]": "password123"} # In prod: use hashed passwords!
def authenticate_admin(self, username: str, password: str) -> bool:
return self.admins.get(username) == password
# Initialize services
school_service = MockSchoolService()
admin_service = MockAdminService()
# Load OpenAI key
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
USE_OPENAI = bool(OPENAI_API_KEY)
if USE_OPENAI:
try:
from openai import OpenAI
openai_client = OpenAI(api_key=OPENAI_API_KEY)
print("βœ… OpenAI client initialized")
except ImportError:
print("⚠️ OpenAI library not installed. Falling back to mock responses.")
USE_OPENAI = False
else:
print("⚠️ OPENAI_API_KEY not set. Using mock AI responses.")
# ==================== AI CHAT FUNCTION ====================
def ai_chat(message: str, history: List) -> str:
"""
Generate AI response using OpenAI or fallback mock.
Injects school context for better, relevant answers.
"""
if USE_OPENAI:
try:
school_context = school_service.format_school_context_for_ai()
system_prompt = f"""You are ThutoAI, a helpful student assistant at a school.
Use this context to answer accurately:
{school_context}
Be supportive, clear, and student-friendly. If asked about exams, assignments, or announcements, refer to the context above.
If unsure, say 'I don't know' rather than guessing."""
response = openai_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()
# Append related info if keywords detected
keywords = ['exam', 'test', 'due', 'assignment', 'announcement']
if any(kw in message.lower() for kw in keywords):
search_results = school_service.search_school_info(message)
if search_results:
reply += "\n\nπŸ“š **Related Info:**\n"
for r in search_results[:2]:
reply += f"β€’ **{r['title']}** ({r['course']}) - {r['content'][:60]}...\n"
return reply
except Exception as e:
return f"⚠️ AI Error: {str(e)}"
else:
# Mock response for demo without API key
return f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\n*(Mock mode β€” set OPENAI_API_KEY for real AI answers)*"
# ==================== GRADIO INTERFACE ====================
# Custom CSS for mobile-friendly, clean design
CUSTOM_CSS = """
.gradio-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.announcement-box {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
background: #fafafa;
}
.announcement-title {
font-weight: bold;
color: #2c3e50;
font-size: 1.1em;
}
.priority-high { color: #e74c3c; }
.priority-normal { color: #f39c12; }
.priority-low { color: #7f8c8d; }
.course-tag {
display: inline-block;
background: #3498db;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8em;
margin-left: 8px;
}
.admin-panel {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
"""
# State to track if user is admin
IS_ADMIN = False
def toggle_admin_login(username: str, password: str) -> tuple:
"""Authenticate admin and toggle UI state."""
global IS_ADMIN
if admin_service.authenticate_admin(username, password):
IS_ADMIN = True
return (
gr.update(visible=False), # Hide login
gr.update(visible=True), # Show dashboard
"βœ… Login successful! Welcome, Admin.",
gr.update(visible=True) # Show post form
)
else:
return (
gr.update(visible=True),
gr.update(visible=False),
"❌ Invalid credentials. Try again.",
gr.update(visible=False)
)
def logout_admin() -> tuple:
"""Logout admin and reset UI."""
global IS_ADMIN
IS_ADMIN = False
return (
gr.update(visible=True),
gr.update(visible=False),
"",
gr.update(visible=False)
)
def post_announcement(title: str, content: str, course: str, priority: str) -> str:
"""Post new announcement and refresh list."""
if not IS_ADMIN:
return "πŸ”’ Only admins can post announcements."
if not title or not content:
return "⚠️ Title and content required."
school_service.add_announcement(title, content, course, priority)
return "βœ… Announcement posted successfully!"
def display_announcements(course_filter: str) -> str:
"""Render announcements with HTML for styling."""
announcements = school_service.get_announcements_by_course(course_filter)
if not announcements:
return "<p style='color: #7f8c8d;'>No announcements found.</p>"
html = ""
for ann in announcements:
priority_class = f"priority-{ann['priority']}"
html += f"""
<div class="announcement-box">
<div>
<span class="announcement-title">{ann['title']}</span>
<span class="course-tag">{ann['course']}</span>
</div>
<div style="margin-top: 8px;">{ann['content']}</div>
<div style="margin-top: 8px; font-size: 0.9em; color: #7f8c8d;">
Posted on {ann['date']} Β· <span class="{priority_class}">Priority: {ann['priority'].title()}</span>
</div>
</div>
"""
return html
def file_upload_status(file) -> str:
"""Handle file upload and return status."""
return school_service.upload_file(file)
# Build Gradio app
with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo:
gr.Markdown("# πŸŽ“ ThutoAI - Student Assistant")
with gr.Tab("πŸ“’ Announcements"):
gr.Markdown("### Latest Updates from Your Courses")
course_dropdown = gr.Dropdown(
choices=["All"] + school_service.courses,
value="All",
label="Filter by Course",
interactive=True
)
announcement_display = gr.HTML(label="Announcements")
course_dropdown.change(fn=display_announcements, inputs=course_dropdown, outputs=announcement_display)
demo.load(fn=display_announcements, inputs=course_dropdown, outputs=announcement_display) # Initial load
with gr.Tab("πŸ’¬ Ask AI Assistant"):
gr.Markdown("### Ask any question β€” homework help, deadlines, school info, and more!")
chatbot = gr.Chatbot(height=400)
msg = gr.Textbox(label="Type your question", placeholder="E.g., When is the Math exam?")
clear = gr.Button("Clear Chat")
def respond(message, chat_history):
bot_message = ai_chat(message, chat_history)
chat_history.append((message, bot_message))
return "", chat_history
msg.submit(respond, [msg, chatbot], [msg, chatbot])
clear.click(lambda: None, None, chatbot, queue=False)
with gr.Tab("πŸ“‚ Upload Files"):
gr.Markdown("### Upload study materials (PDFs, notes, etc.)")
file_input = gr.File(label="Choose a file")
upload_btn = gr.Button("Upload")
upload_status = gr.Textbox(label="Status", interactive=False)
upload_btn.click(fn=file_upload_status, inputs=file_input, outputs=upload_status)
with gr.Tab("πŸ” Admin Panel"):
gr.Markdown("### Teacher/Admin Login")
with gr.Group() as login_group:
username = gr.Textbox(label="Username", placeholder="e.g., [email protected]")
password = gr.Textbox(label="Password", type="password")
login_btn = gr.Button("Login")
login_status = gr.Textbox(label="Status", interactive=False)
with gr.Group(visible=False) as admin_dashboard:
gr.Markdown("## ✍️ Post New Announcement")
with gr.Row():
admin_title = gr.Textbox(label="Title", placeholder="Enter announcement title")
admin_course = gr.Dropdown(choices=school_service.courses, label="Course", value="General")
admin_content = gr.TextArea(label="Content", placeholder="Enter full announcement details...")
admin_priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal")
post_btn = gr.Button("Post Announcement")
post_status = gr.Textbox(label="Result", interactive=False)
post_btn.click(
fn=post_announcement,
inputs=[admin_title, admin_content, admin_course, admin_priority],
outputs=post_status
)
logout_btn = gr.Button("Logout")
logout_btn.click(
fn=logout_admin,
inputs=None,
outputs=[login_group, admin_dashboard, login_status, post_status]
)
login_btn.click(
fn=toggle_admin_login,
inputs=[username, password],
outputs=[login_group, admin_dashboard, login_status, post_status]
)
# Launch app
if __name__ == "__main__":
demo.launch()