Spaces:
Runtime error
Runtime error
| """ | |
| 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() |