ngwakomadikwe commited on
Commit
bc34aee
Β·
verified Β·
1 Parent(s): 36a0bc6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +254 -184
app.py CHANGED
@@ -1,36 +1,67 @@
1
  """
2
- ThutoAI - Complete Student Assistant with School Management
3
- Refactored for Hugging Face Spaces using Gradio (Flask won't work here)
4
- Enhanced with mobile UI, course filtering, admin panel, and smarter AI chat.
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  import os
8
- import json
9
- from datetime import datetime
10
- from typing import List, Dict, Any, Optional
11
  import gradio as gr
 
 
12
 
13
- # ==================== MOCK SERVICES (Replace with real DB logic) ====================
14
 
15
- class MockSchoolService:
16
- """Simulates school data services. Replace with real DB/API calls."""
17
 
18
  def __init__(self):
19
- # Sample data β€” replace with database queries in production
20
  self.announcements = [
21
- {"id": 1, "title": "Math Exam Reminder", "content": "Final exam next Monday. Chapters 1-5.", "course": "Mathematics", "date": "2025-04-10", "priority": "high"},
22
- {"id": 2, "title": "Science Fair", "content": "Submit projects by Friday.", "course": "Science", "date": "2025-04-08", "priority": "normal"},
23
- {"id": 3, "title": "Library Hours Update", "content": "Open until 8 PM during exams.", "course": "General", "date": "2025-04-07", "priority": "low"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ]
25
- self.courses = ["Mathematics", "Science", "English", "History", "General"]
26
- self.files = [] # Simulate uploaded files
27
 
28
- def get_announcements_by_course(self, course: str = "All") -> List[Dict]:
29
- if course == "All":
 
30
  return self.announcements
31
- return [a for a in self.announcements if a["course"] == course]
32
 
33
  def add_announcement(self, title: str, content: str, course: str, priority: str):
 
34
  new_id = max([a["id"] for a in self.announcements], default=0) + 1
35
  self.announcements.append({
36
  "id": new_id,
@@ -41,40 +72,44 @@ class MockSchoolService:
41
  "priority": priority
42
  })
43
 
44
- def upload_file(self, file):
45
- if file:
46
- self.files.append({"name": file.name, "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")})
47
- return f"βœ… Uploaded: {file.name}" if file else "❌ No file selected"
48
-
49
- def search_school_info(self, query: str) -> List[Dict]:
50
- results = []
51
- for ann in self.announcements:
52
- if query.lower() in ann["title"].lower() or query.lower() in ann["content"].lower():
53
- results.append(ann)
54
- return results
55
-
56
- def format_school_context_for_ai(self) -> str:
57
- context = "Current School Context:\n"
58
- for ann in self.announcements[:5]: # Top 5 recent
59
- context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:50]}... (Priority: {ann['priority']})\n"
60
  return context
61
 
62
 
63
- class MockAdminService:
64
- """Simulates admin authentication and management."""
65
 
66
  def __init__(self):
67
- self.admins = {"[email protected]": "password123"} # In prod: use hashed passwords!
 
 
 
 
68
 
69
- def authenticate_admin(self, username: str, password: str) -> bool:
70
- return self.admins.get(username) == password
 
71
 
72
 
73
  # Initialize services
74
- school_service = MockSchoolService()
75
- admin_service = MockAdminService()
76
 
77
- # Load OpenAI key
78
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
79
  USE_OPENAI = bool(OPENAI_API_KEY)
80
 
@@ -82,29 +117,37 @@ if USE_OPENAI:
82
  try:
83
  from openai import OpenAI
84
  openai_client = OpenAI(api_key=OPENAI_API_KEY)
85
- print("βœ… OpenAI client initialized")
86
- except ImportError:
87
- print("⚠️ OpenAI library not installed. Falling back to mock responses.")
88
  USE_OPENAI = False
89
  else:
90
- print("⚠️ OPENAI_API_KEY not set. Using mock AI responses.")
91
 
92
  # ==================== AI CHAT FUNCTION ====================
93
 
94
- def ai_chat(message: str, history: List) -> str:
95
  """
96
- Generate AI response using OpenAI or fallback mock.
97
- Injects school context for better, relevant answers.
98
  """
99
  if USE_OPENAI:
100
  try:
101
- school_context = school_service.format_school_context_for_ai()
102
- system_prompt = f"""You are ThutoAI, a helpful student assistant at a school.
103
- Use this context to answer accurately:
104
- {school_context}
105
 
106
- Be supportive, clear, and student-friendly. If asked about exams, assignments, or announcements, refer to the context above.
107
- If unsure, say 'I don't know' rather than guessing."""
 
 
 
 
 
 
 
 
 
 
108
 
109
  response = openai_client.chat.completions.create(
110
  model="gpt-3.5-turbo",
@@ -117,205 +160,232 @@ If unsure, say 'I don't know' rather than guessing."""
117
  )
118
  reply = response.choices[0].message.content.strip()
119
 
120
- # Append related info if keywords detected
121
- keywords = ['exam', 'test', 'due', 'assignment', 'announcement']
122
  if any(kw in message.lower() for kw in keywords):
123
- search_results = school_service.search_school_info(message)
124
- if search_results:
125
- reply += "\n\nπŸ“š **Related Info:**\n"
126
- for r in search_results[:2]:
127
- reply += f"β€’ **{r['title']}** ({r['course']}) - {r['content'][:60]}...\n"
 
128
 
129
  return reply
130
 
131
  except Exception as e:
132
- return f"⚠️ AI Error: {str(e)}"
133
-
134
  else:
135
- # Mock response for demo without API key
136
- return f"πŸ‘‹ Hi! I'm ThutoAI. You asked: '{message}'.\n*(Mock mode β€” set OPENAI_API_KEY for real AI answers)*"
137
 
138
 
139
- # ==================== GRADIO INTERFACE ====================
140
 
141
- # Custom CSS for mobile-friendly, clean design
142
- CUSTOM_CSS = """
143
- .gradio-container {
144
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
145
- }
146
- .announcement-box {
147
- border: 1px solid #e0e0e0;
148
- border-radius: 8px;
149
- padding: 16px;
150
- margin-bottom: 12px;
151
- background: #fafafa;
152
- }
153
- .announcement-title {
154
- font-weight: bold;
155
- color: #2c3e50;
156
- font-size: 1.1em;
157
- }
158
- .priority-high { color: #e74c3c; }
159
- .priority-normal { color: #f39c12; }
160
- .priority-low { color: #7f8c8d; }
161
- .course-tag {
162
- display: inline-block;
163
- background: #3498db;
164
- color: white;
165
- padding: 2px 8px;
166
- border-radius: 4px;
167
- font-size: 0.8em;
168
- margin-left: 8px;
169
- }
170
- .admin-panel {
171
- background: #f8f9fa;
172
- padding: 20px;
173
- border-radius: 8px;
174
- border: 1px solid #dee2e6;
175
- }
176
- """
177
 
178
- # State to track if user is admin
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  IS_ADMIN = False
180
 
181
- def toggle_admin_login(username: str, password: str) -> tuple:
182
- """Authenticate admin and toggle UI state."""
183
  global IS_ADMIN
184
- if admin_service.authenticate_admin(username, password):
185
  IS_ADMIN = True
186
  return (
187
- gr.update(visible=False), # Hide login
188
  gr.update(visible=True), # Show dashboard
189
- "βœ… Login successful! Welcome, Admin.",
190
  gr.update(visible=True) # Show post form
191
  )
192
  else:
193
  return (
194
  gr.update(visible=True),
195
  gr.update(visible=False),
196
- "❌ Invalid credentials. Try again.",
197
  gr.update(visible=False)
198
  )
199
 
200
- def logout_admin() -> tuple:
201
- """Logout admin and reset UI."""
202
  global IS_ADMIN
203
  IS_ADMIN = False
204
  return (
205
- gr.update(visible=True),
206
- gr.update(visible=False),
207
  "",
208
- gr.update(visible=False)
209
  )
210
 
211
- def post_announcement(title: str, content: str, course: str, priority: str) -> str:
212
- """Post new announcement and refresh list."""
213
  if not IS_ADMIN:
214
- return "πŸ”’ Only admins can post announcements."
215
- if not title or not content:
216
- return "⚠️ Title and content required."
217
  school_service.add_announcement(title, content, course, priority)
218
- return "βœ… Announcement posted successfully!"
219
 
220
- def display_announcements(course_filter: str) -> str:
221
- """Render announcements with HTML for styling."""
222
- announcements = school_service.get_announcements_by_course(course_filter)
223
- if not announcements:
224
- return "<p style='color: #7f8c8d;'>No announcements found.</p>"
225
 
226
- html = ""
227
- for ann in announcements:
228
- priority_class = f"priority-{ann['priority']}"
229
- html += f"""
230
- <div class="announcement-box">
231
- <div>
232
- <span class="announcement-title">{ann['title']}</span>
233
- <span class="course-tag">{ann['course']}</span>
234
- </div>
235
- <div style="margin-top: 8px;">{ann['content']}</div>
236
- <div style="margin-top: 8px; font-size: 0.9em; color: #7f8c8d;">
237
- Posted on {ann['date']} Β· <span class="{priority_class}">Priority: {ann['priority'].title()}</span>
238
- </div>
239
- </div>
240
- """
241
- return html
 
 
 
 
 
 
 
242
 
243
- def file_upload_status(file) -> str:
244
- """Handle file upload and return status."""
245
- return school_service.upload_file(file)
246
 
247
- # Build Gradio app
248
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo:
249
- gr.Markdown("# πŸŽ“ ThutoAI - Student Assistant")
250
 
 
251
  with gr.Tab("πŸ“’ Announcements"):
252
- gr.Markdown("### Latest Updates from Your Courses")
253
  course_dropdown = gr.Dropdown(
254
- choices=["All"] + school_service.courses,
255
  value="All",
256
- label="Filter by Course",
257
  interactive=True
258
  )
259
- announcement_display = gr.HTML(label="Announcements")
260
- course_dropdown.change(fn=display_announcements, inputs=course_dropdown, outputs=announcement_display)
261
- demo.load(fn=display_announcements, inputs=course_dropdown, outputs=announcement_display) # Initial load
 
 
 
 
 
 
 
 
 
262
 
263
- with gr.Tab("πŸ’¬ Ask AI Assistant"):
264
- gr.Markdown("### Ask any question β€” homework help, deadlines, school info, and more!")
265
- chatbot = gr.Chatbot(height=400)
266
- msg = gr.Textbox(label="Type your question", placeholder="E.g., When is the Math exam?")
267
- clear = gr.Button("Clear Chat")
 
268
 
269
  def respond(message, chat_history):
270
- bot_message = ai_chat(message, chat_history)
271
- chat_history.append((message, bot_message))
272
  return "", chat_history
273
 
274
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
275
- clear.click(lambda: None, None, chatbot, queue=False)
276
 
277
- with gr.Tab("πŸ“‚ Upload Files"):
278
- gr.Markdown("### Upload study materials (PDFs, notes, etc.)")
279
- file_input = gr.File(label="Choose a file")
280
- upload_btn = gr.Button("Upload")
 
281
  upload_status = gr.Textbox(label="Status", interactive=False)
282
- upload_btn.click(fn=file_upload_status, inputs=file_input, outputs=upload_status)
 
 
 
 
283
 
284
- with gr.Tab("πŸ” Admin Panel"):
285
- gr.Markdown("### Teacher/Admin Login")
 
286
 
 
287
  with gr.Group() as login_group:
288
- username = gr.Textbox(label="Username", placeholder="e.g., [email protected]")
289
- password = gr.Textbox(label="Password", type="password")
290
- login_btn = gr.Button("Login")
291
- login_status = gr.Textbox(label="Status", interactive=False)
 
292
 
 
293
  with gr.Group(visible=False) as admin_dashboard:
294
- gr.Markdown("## ✍️ Post New Announcement")
295
  with gr.Row():
296
- admin_title = gr.Textbox(label="Title", placeholder="Enter announcement title")
297
- admin_course = gr.Dropdown(choices=school_service.courses, label="Course", value="General")
298
- admin_content = gr.TextArea(label="Content", placeholder="Enter full announcement details...")
299
- admin_priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal")
300
- post_btn = gr.Button("Post Announcement")
301
  post_status = gr.Textbox(label="Result", interactive=False)
302
 
 
303
  post_btn.click(
304
- fn=post_announcement,
305
- inputs=[admin_title, admin_content, admin_course, admin_priority],
306
  outputs=post_status
307
  )
308
 
309
- logout_btn = gr.Button("Logout")
 
310
  logout_btn.click(
311
- fn=logout_admin,
312
  inputs=None,
313
  outputs=[login_group, admin_dashboard, login_status, post_status]
314
  )
315
 
 
316
  login_btn.click(
317
- fn=toggle_admin_login,
318
- inputs=[username, password],
319
  outputs=[login_group, admin_dashboard, login_status, post_status]
320
  )
321
 
 
1
  """
2
+ SmartMate - AI Student Assistant (Upgraded for Hugging Face Spaces)
3
+ Author: ngwakomadikwe
4
+ Enhanced with: course filtering, admin panel, mobile UI, smarter AI context.
5
+
6
+ Features:
7
+ - Filter announcements by course/subject
8
+ - Upload files (PDFs, notes)
9
+ - AI chatbot with school context (OpenAI)
10
+ - Admin login to post announcements
11
+ - Responsive, beautiful UI
12
+
13
+ Deployed on Hugging Face Spaces β†’ https://huggingface.co/spaces/ngwakomadikwe/SmartMate
14
  """
15
 
16
  import os
 
 
 
17
  import gradio as gr
18
+ from datetime import datetime
19
+ from typing import List, Dict
20
 
21
+ # ==================== MOCK DATA SERVICES (Replace with DB in future) ====================
22
 
23
+ class SchoolService:
24
+ """Manages announcements, files, and school context for AI."""
25
 
26
  def __init__(self):
27
+ # Sample announcements β€” you can replace this with database later
28
  self.announcements = [
29
+ {
30
+ "id": 1,
31
+ "title": "Math Final Exam",
32
+ "content": "Covers chapters 1-5. Bring calculator.",
33
+ "course": "Mathematics",
34
+ "date": "2025-04-15",
35
+ "priority": "high"
36
+ },
37
+ {
38
+ "id": 2,
39
+ "title": "Science Project Due",
40
+ "content": "Submit your ecology report by Friday.",
41
+ "course": "Science",
42
+ "date": "2025-04-12",
43
+ "priority": "normal"
44
+ },
45
+ {
46
+ "id": 3,
47
+ "title": "Library Extended Hours",
48
+ "content": "Open until 9 PM during exam week.",
49
+ "course": "General",
50
+ "date": "2025-04-10",
51
+ "priority": "low"
52
+ },
53
  ]
54
+ self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
55
+ self.uploaded_files = []
56
 
57
+ def get_announcements(self, course_filter: str = "All") -> List[Dict]:
58
+ """Return announcements filtered by course."""
59
+ if course_filter == "All":
60
  return self.announcements
61
+ return [a for a in self.announcements if a["course"] == course_filter]
62
 
63
  def add_announcement(self, title: str, content: str, course: str, priority: str):
64
+ """Add new announcement (used by admin)."""
65
  new_id = max([a["id"] for a in self.announcements], default=0) + 1
66
  self.announcements.append({
67
  "id": new_id,
 
72
  "priority": priority
73
  })
74
 
75
+ def upload_file(self, file) -> str:
76
+ """Simulate file upload."""
77
+ if not file:
78
+ return "❌ No file selected"
79
+ self.uploaded_files.append({
80
+ "name": file.name,
81
+ "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
82
+ })
83
+ return f"βœ… Uploaded: {file.name}"
84
+
85
+ def get_school_context_for_ai(self) -> str:
86
+ """Return formatted context for AI to use in responses."""
87
+ context = "Current School Announcements (for context):\n"
88
+ for ann in self.announcements[:5]: # Limit to 5 most recent
89
+ context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:60]}... (Priority: {ann['priority']})\n"
 
90
  return context
91
 
92
 
93
+ class AdminService:
94
+ """Handles admin authentication."""
95
 
96
  def __init__(self):
97
+ # In production, store hashed passwords in a database!
98
+ self.admin_credentials = {
99
+ "[email protected]": "password123",
100
+ "[email protected]": "letmein"
101
+ }
102
 
103
+ def authenticate(self, username: str, password: str) -> bool:
104
+ """Check if username/password is valid."""
105
+ return self.admin_credentials.get(username) == password
106
 
107
 
108
  # Initialize services
109
+ school_service = SchoolService()
110
+ admin_service = AdminService()
111
 
112
+ # Check if OpenAI key is set
113
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
114
  USE_OPENAI = bool(OPENAI_API_KEY)
115
 
 
117
  try:
118
  from openai import OpenAI
119
  openai_client = OpenAI(api_key=OPENAI_API_KEY)
120
+ print("βœ… OpenAI client ready")
121
+ except Exception as e:
122
+ print(f"⚠️ Error initializing OpenAI: {e}")
123
  USE_OPENAI = False
124
  else:
125
+ print("⚠️ OPENAI_API_KEY not set. AI will use mock responses.")
126
 
127
  # ==================== AI CHAT FUNCTION ====================
128
 
129
+ def smart_chat(message: str, history: List) -> str:
130
  """
131
+ Generate AI response using OpenAI (or mock if no key).
132
+ Injects school announcements as context for better, relevant answers.
133
  """
134
  if USE_OPENAI:
135
  try:
136
+ # Get school context to make AI responses relevant
137
+ context = school_service.get_school_context_for_ai()
 
 
138
 
139
+ system_prompt = f"""You are SmartMate, a friendly AI assistant for students.
140
+ Use the school context below to give accurate, helpful answers.
141
+
142
+ {context}
143
+
144
+ Guidelines:
145
+ - Be encouraging and supportive.
146
+ - If asked about exams, assignments, or announcements, refer to the context.
147
+ - Offer study tips if appropriate.
148
+ - If unsure, say so β€” don't guess.
149
+
150
+ Now respond to the student's question:"""
151
 
152
  response = openai_client.chat.completions.create(
153
  model="gpt-3.5-turbo",
 
160
  )
161
  reply = response.choices[0].message.content.strip()
162
 
163
+ # If question relates to school, append matching announcements
164
+ keywords = ["exam", "test", "due", "assignment", "announcement", "when", "what", "deadline"]
165
  if any(kw in message.lower() for kw in keywords):
166
+ matches = school_service.get_announcements()
167
+ relevant = [a for a in matches if any(kw in a["title"].lower() or kw in a["content"].lower() for kw in keywords)]
168
+ if relevant:
169
+ reply += "\n\nπŸ“Œ **Relevant Announcements:**"
170
+ for ann in relevant[:2]: # Show top 2
171
+ reply += f"\n- **{ann['title']}** ({ann['course']}) β†’ {ann['content'][:70]}..."
172
 
173
  return reply
174
 
175
  except Exception as e:
176
+ return f"⚠️ Sorry, I had trouble thinking. Error: {str(e)}"
 
177
  else:
178
+ # Mock response if no API key
179
+ return f"πŸ‘‹ Hi! I’m SmartMate. You asked: '{message}'.\n*(Tip: Set OPENAI_API_KEY in HF Secrets for real AI answers.)*"
180
 
181
 
182
+ # ==================== UI RENDERING HELPERS ====================
183
 
184
+ def render_announcements(course: str) -> str:
185
+ """Generate HTML to display announcements with styling."""
186
+ announcements = school_service.get_announcements(course)
187
+ if not announcements:
188
+ return "<p style='color: #6c757d; text-align: center;'>No announcements for this course.</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ html = ""
191
+ priority_colors = {"high": "#dc3545", "normal": "#ffc107", "low": "#6c757d"}
192
+
193
+ for ann in announcements:
194
+ color = priority_colors.get(ann["priority"], "#6c757d")
195
+ html += f"""
196
+ <div style="
197
+ border: 1px solid #e9ecef;
198
+ border-radius: 8px;
199
+ padding: 16px;
200
+ margin-bottom: 12px;
201
+ background: #f8f9fa;
202
+ ">
203
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
204
+ <strong style="font-size: 1.1em; color: #212529;">{ann['title']}</strong>
205
+ <span style="
206
+ background: {color};
207
+ color: white;
208
+ padding: 3px 8px;
209
+ border-radius: 4px;
210
+ font-size: 0.8em;
211
+ font-weight: bold;
212
+ ">{ann['priority'].upper()}</span>
213
+ </div>
214
+ <div style="margin-bottom: 8px;">{ann['content']}</div>
215
+ <div style="font-size: 0.85em; color: #6c757d;">
216
+ πŸ“š {ann['course']} &nbsp; | &nbsp; πŸ“… {ann['date']}
217
+ </div>
218
+ </div>
219
+ """
220
+ return html
221
+
222
+
223
+ # ==================== ADMIN STATE & FUNCTIONS ====================
224
+
225
+ # Track if user is logged in as admin (simple state β€” not secure for production)
226
  IS_ADMIN = False
227
 
228
+ def admin_login(username: str, password: str) -> tuple:
229
+ """Attempt to log in admin. Returns UI update instructions."""
230
  global IS_ADMIN
231
+ if admin_service.authenticate(username, password):
232
  IS_ADMIN = True
233
  return (
234
+ gr.update(visible=False), # Hide login form
235
  gr.update(visible=True), # Show dashboard
236
+ "βœ… Login successful! You can now post announcements.",
237
  gr.update(visible=True) # Show post form
238
  )
239
  else:
240
  return (
241
  gr.update(visible=True),
242
  gr.update(visible=False),
243
+ "❌ Invalid username or password. Try again.",
244
  gr.update(visible=False)
245
  )
246
 
247
+ def admin_logout() -> tuple:
248
+ """Log out admin."""
249
  global IS_ADMIN
250
  IS_ADMIN = False
251
  return (
252
+ gr.update(visible=True), # Show login
253
+ gr.update(visible=False), # Hide dashboard
254
  "",
255
+ gr.update(visible=False) # Hide post form
256
  )
257
 
258
+ def admin_post_announcement(title: str, content: str, course: str, priority: str) -> str:
259
+ """Post new announcement if admin."""
260
  if not IS_ADMIN:
261
+ return "πŸ”’ You must be logged in as admin to post announcements."
262
+ if not title.strip() or not content.strip():
263
+ return "⚠️ Title and content cannot be empty."
264
  school_service.add_announcement(title, content, course, priority)
265
+ return "βœ… Announcement posted successfully! Refresh announcements to see it."
266
 
267
+ # ==================== CUSTOM CSS FOR MOBILE & BEAUTY ====================
 
 
 
 
268
 
269
+ CUSTOM_CSS = """
270
+ .gradio-container {
271
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
272
+ }
273
+ .announcement-box {
274
+ border: 1px solid #e0e0e0;
275
+ border-radius: 10px;
276
+ padding: 16px;
277
+ margin: 10px 0;
278
+ background: white;
279
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
280
+ }
281
+ .priority-badge {
282
+ font-size: 0.75em;
283
+ padding: 3px 8px;
284
+ border-radius: 4px;
285
+ font-weight: bold;
286
+ color: white;
287
+ }
288
+ .high { background: #e74c3c; }
289
+ .normal { background: #f39c12; }
290
+ .low { background: #7f8c8d; }
291
+ """
292
 
293
+ # ==================== BUILD GRADIO INTERFACE ====================
 
 
294
 
 
295
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo:
296
+ gr.Markdown("# πŸŽ“ SmartMate β€” Your AI School Assistant")
297
 
298
+ # ========= TAB: Announcements =========
299
  with gr.Tab("πŸ“’ Announcements"):
300
+ gr.Markdown("### Filter by Course/Subject")
301
  course_dropdown = gr.Dropdown(
302
+ choices=school_service.courses,
303
  value="All",
304
+ label="Select Course",
305
  interactive=True
306
  )
307
+ announcements_html = gr.HTML()
308
+ course_dropdown.change(
309
+ fn=render_announcements,
310
+ inputs=course_dropdown,
311
+ outputs=announcements_html
312
+ )
313
+ # Load initial announcements
314
+ demo.load(
315
+ fn=render_announcements,
316
+ inputs=course_dropdown,
317
+ outputs=announcements_html
318
+ )
319
 
320
+ # ========= TAB: AI Chat =========
321
+ with gr.Tab("πŸ’¬ Ask SmartMate"):
322
+ gr.Markdown("### Ask me anything β€” homework, deadlines, school info, or just for motivation!")
323
+ chatbot = gr.Chatbot(height=450, bubble_full_width=False)
324
+ msg = gr.Textbox(label="Type your question", placeholder="E.g., What's due this week in Math?")
325
+ clear_btn = gr.Button("πŸ—‘οΈ Clear Chat")
326
 
327
  def respond(message, chat_history):
328
+ bot_reply = smart_chat(message, chat_history)
329
+ chat_history.append((message, bot_reply))
330
  return "", chat_history
331
 
332
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
333
+ clear_btn.click(lambda: None, None, chatbot, queue=False)
334
 
335
+ # ========= TAB: Upload Files =========
336
+ with gr.Tab("πŸ“‚ Upload Notes/PDFs"):
337
+ gr.Markdown("### Upload study materials for personal reference (not yet stored long-term)")
338
+ file_uploader = gr.File(label="Choose a file (PDF, DOC, TXT, etc.)")
339
+ upload_btn = gr.Button("πŸ“€ Upload")
340
  upload_status = gr.Textbox(label="Status", interactive=False)
341
+ upload_btn.click(
342
+ fn=school_service.upload_file,
343
+ inputs=file_uploader,
344
+ outputs=upload_status
345
+ )
346
 
347
+ # ========= TAB: Admin Panel =========
348
+ with gr.Tab("πŸ” Admin (Teachers)"):
349
+ gr.Markdown("### Post Announcements to Students")
350
 
351
+ # Login Form
352
  with gr.Group() as login_group:
353
+ gr.Markdown("#### Login to Post Announcements")
354
+ admin_username = gr.Textbox(label="Username", placeholder="e.g., [email protected]")
355
+ admin_password = gr.Textbox(label="Password", type="password", placeholder="β€’β€’β€’β€’β€’β€’β€’β€’")
356
+ login_btn = gr.Button("πŸ”“ Login")
357
+ login_status = gr.Textbox(label="Login Status", interactive=False)
358
 
359
+ # Admin Dashboard (hidden until login)
360
  with gr.Group(visible=False) as admin_dashboard:
361
+ gr.Markdown("#### ✍️ Create New Announcement")
362
  with gr.Row():
363
+ ann_title = gr.Textbox(label="Title", placeholder="e.g., Science Quiz Moved")
364
+ ann_course = gr.Dropdown(choices=school_service.courses[1:], label="Course", value="General")
365
+ ann_content = gr.Textbox(label="Content", placeholder="Details for students...", lines=3)
366
+ ann_priority = gr.Radio(["low", "normal", "high"], label="Priority", value="normal")
367
+ post_btn = gr.Button("πŸ“¬ Post Announcement")
368
  post_status = gr.Textbox(label="Result", interactive=False)
369
 
370
+ # Post logic
371
  post_btn.click(
372
+ fn=admin_post_announcement,
373
+ inputs=[ann_title, ann_content, ann_course, ann_priority],
374
  outputs=post_status
375
  )
376
 
377
+ # Logout button
378
+ logout_btn = gr.Button("⬅️ Logout")
379
  logout_btn.click(
380
+ fn=admin_logout,
381
  inputs=None,
382
  outputs=[login_group, admin_dashboard, login_status, post_status]
383
  )
384
 
385
+ # Login logic
386
  login_btn.click(
387
+ fn=admin_login,
388
+ inputs=[admin_username, admin_password],
389
  outputs=[login_group, admin_dashboard, login_status, post_status]
390
  )
391