Spaces:
Runtime error
Runtime error
File size: 12,293 Bytes
31ac6a0 ed2729e 31ac6a0 ed2729e 64b4650 31ac6a0 ed2729e 31ac6a0 ed2729e eb69519 ed2729e 64b4650 ed2729e c0bc8cc 64b4650 ed2729e 64b4650 ed2729e 31ac6a0 ed2729e 31ac6a0 64b4650 ed2729e 31ac6a0 ed2729e 31ac6a0 ed2729e 31ac6a0 ed2729e 31ac6a0 ed2729e 9c74f73 ed2729e 64b4650 ed2729e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
"""
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() |