ngwakomadikwe commited on
Commit
f16e071
Β·
verified Β·
1 Parent(s): 0873d37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -239
app.py CHANGED
@@ -1,16 +1,6 @@
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
@@ -18,13 +8,10 @@ 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,
@@ -52,16 +39,14 @@ class SchoolService:
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,
@@ -73,83 +58,53 @@ class SchoolService:
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
- "teacher@smartmate.edu": "password123",
100
- "admin@school.org": "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
 
116
  if USE_OPENAI:
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",
154
  messages=[
155
  {"role": "system", "content": system_prompt},
@@ -160,235 +115,136 @@ Now respond to the student's question:"""
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., teacher@smartmate.edu")
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
 
392
- # Launch app
393
  if __name__ == "__main__":
394
  demo.launch()
 
1
  """
2
+ SmartMate - AI Student Assistant (Gradio Version)
3
+ For Hugging Face Spaces
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
  import os
 
8
  from datetime import datetime
9
  from typing import List, Dict
10
 
11
+ # ==================== MOCK SERVICES ====================
12
 
13
  class SchoolService:
 
 
14
  def __init__(self):
 
15
  self.announcements = [
16
  {
17
  "id": 1,
 
39
  },
40
  ]
41
  self.courses = ["All", "Mathematics", "Science", "English", "History", "General"]
42
+ self.files = []
43
 
44
  def get_announcements(self, course_filter: str = "All") -> List[Dict]:
 
45
  if course_filter == "All":
46
  return self.announcements
47
  return [a for a in self.announcements if a["course"] == course_filter]
48
 
49
  def add_announcement(self, title: str, content: str, course: str, priority: str):
 
50
  new_id = max([a["id"] for a in self.announcements], default=0) + 1
51
  self.announcements.append({
52
  "id": new_id,
 
58
  })
59
 
60
  def upload_file(self, file) -> str:
 
61
  if not file:
62
  return "❌ No file selected"
63
+ self.files.append({"name": file.name, "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")})
 
 
 
64
  return f"βœ… Uploaded: {file.name}"
65
 
66
  def get_school_context_for_ai(self) -> str:
67
+ context = "Current School Announcements:\n"
68
+ for ann in self.announcements[:5]:
 
69
  context += f"- [{ann['course']}] {ann['title']}: {ann['content'][:60]}... (Priority: {ann['priority']})\n"
70
  return context
71
 
72
 
73
  class AdminService:
 
 
74
  def __init__(self):
75
+ self.admins = {"teacher@smartmate.edu": "password123"}
 
 
 
 
76
 
77
  def authenticate(self, username: str, password: str) -> bool:
78
+ return self.admins.get(username) == password
 
79
 
80
 
81
  # Initialize services
82
  school_service = SchoolService()
83
  admin_service = AdminService()
84
 
85
+ # OpenAI setup
86
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
87
  USE_OPENAI = bool(OPENAI_API_KEY)
88
 
89
  if USE_OPENAI:
90
  try:
91
  from openai import OpenAI
92
+ client = OpenAI(api_key=OPENAI_API_KEY)
 
93
  except Exception as e:
94
+ print(f"⚠️ OpenAI error: {e}")
95
  USE_OPENAI = False
96
  else:
97
+ print("⚠️ OPENAI_API_KEY not set. Using mock responses.")
98
 
99
+ def ai_chat(message: str, history: List) -> str:
 
 
 
 
 
 
100
  if USE_OPENAI:
101
  try:
102
+ system_prompt = f"""You are SmartMate, an AI assistant for students.
103
+ Use this context to answer accurately:
104
+ {school_service.get_school_context_for_ai()}
 
 
105
 
106
+ Be helpful, encouraging, and accurate. If unsure, say so."""
107
+ response = client.chat.completions.create(
 
 
 
 
 
 
 
 
 
108
  model="gpt-3.5-turbo",
109
  messages=[
110
  {"role": "system", "content": system_prompt},
 
115
  )
116
  reply = response.choices[0].message.content.strip()
117
 
118
+ # Add related announcements
119
+ keywords = ["exam", "test", "due", "assignment"]
120
  if any(kw in message.lower() for kw in keywords):
121
  matches = school_service.get_announcements()
122
  relevant = [a for a in matches if any(kw in a["title"].lower() or kw in a["content"].lower() for kw in keywords)]
123
  if relevant:
124
  reply += "\n\nπŸ“Œ **Relevant Announcements:**"
125
+ for ann in relevant[:2]:
126
+ reply += f"\n- {ann['title']} ({ann['course']}) β†’ {ann['content'][:70]}..."
127
 
128
  return reply
 
129
  except Exception as e:
130
+ return f"⚠️ AI Error: {str(e)}"
131
  else:
132
+ return f"πŸ‘‹ Hi! I'm SmartMate. You asked: '{message}'. *(Set OPENAI_API_KEY in HF Secrets for real AI.)*"
 
133
 
134
 
135
+ # ==================== GRADIO INTERFACE ====================
136
 
137
  def render_announcements(course: str) -> str:
 
138
  announcements = school_service.get_announcements(course)
139
  if not announcements:
140
+ return "<p style='color: #6c757d; text-align: center;'>No announcements.</p>"
 
141
  html = ""
 
 
142
  for ann in announcements:
143
+ color = "red" if ann["priority"] == "high" else "orange" if ann["priority"] == "normal" else "gray"
144
  html += f"""
145
  <div style="
146
+ border: 1px solid #e0e0e0;
147
  border-radius: 8px;
148
+ padding: 12px;
149
+ margin: 8px 0;
150
+ background: white;
151
  ">
152
+ <div style="display: flex; justify-content: space-between;">
153
+ <strong>{ann['title']}</strong>
154
+ <span style="background: {color}; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.7em;">{ann['priority'].upper()}</span>
 
 
 
 
 
 
 
 
 
 
 
155
  </div>
156
+ <div style="margin-top: 4px; color: #555;">{ann['content']}</div>
157
+ <div style="font-size: 0.8em; color: #999;">{ann['course']} β€’ {ann['date']}</div>
158
  </div>
159
  """
160
  return html
161
 
162
 
163
+ # State for admin login
 
 
164
  IS_ADMIN = False
165
 
166
  def admin_login(username: str, password: str) -> tuple:
 
167
  global IS_ADMIN
168
  if admin_service.authenticate(username, password):
169
  IS_ADMIN = True
170
  return (
 
 
 
 
 
 
 
 
171
  gr.update(visible=False),
172
+ gr.update(visible=True),
173
+ "βœ… Login successful!",
174
+ gr.update(visible=True)
175
  )
176
+ return (
177
+ gr.update(visible=True),
178
+ gr.update(visible=False),
179
+ "❌ Invalid credentials",
180
+ gr.update(visible=False)
181
+ )
182
 
183
+ def admin_logout():
 
184
  global IS_ADMIN
185
  IS_ADMIN = False
186
  return (
187
+ gr.update(visible=True),
188
+ gr.update(visible=False),
189
  "",
190
+ gr.update(visible=False)
191
  )
192
 
193
+ def post_announcement(title: str, content: str, course: str, priority: str) -> str:
 
194
  if not IS_ADMIN:
195
+ return "πŸ”’ Only admins can post."
196
+ if not title or not content:
197
+ return "⚠️ Title and content required."
198
  school_service.add_announcement(title, content, course, priority)
199
+ return "βœ… Posted!"
200
 
201
+ # ==================== BUILD UI ====================
202
 
203
+ with gr.Blocks(css="""
204
  .gradio-container {
205
+ font-family: 'Inter', sans-serif;
 
 
 
 
 
 
 
 
206
  }
207
+ """) as demo:
208
+ gr.Markdown("# πŸŽ“ SmartMate - AI Student Assistant")
 
 
 
 
 
 
 
 
 
209
 
 
 
 
 
 
 
210
  with gr.Tab("πŸ“’ Announcements"):
211
+ course_filter = gr.Dropdown(choices=school_service.courses, value="All", label="Filter by Course")
 
 
 
 
 
 
212
  announcements_html = gr.HTML()
213
+ course_filter.change(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
214
+ demo.load(fn=render_announcements, inputs=course_filter, outputs=announcements_html)
215
+
216
+ with gr.Tab("πŸ’¬ Chat"):
217
+ chatbot = gr.Chatbot(height=400)
218
+ msg = gr.Textbox(label="Ask me anything...")
219
+ clear = gr.Button("Clear")
220
+ msg.submit(fn=ai_chat, inputs=[msg, chatbot], outputs=[msg, chatbot])
221
+ clear.click(lambda: None, None, chatbot)
222
+
223
+ with gr.Tab("πŸ“‚ Upload Files"):
224
+ file_input = gr.File(label="Upload PDF/Notes")
225
+ upload_btn = gr.Button("Upload")
226
+ status = gr.Textbox(label="Status")
227
+ upload_btn.click(fn=school_service.upload_file, inputs=file_input, outputs=status)
228
+
229
+ with gr.Tab("πŸ” Admin"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  with gr.Group() as login_group:
231
+ username = gr.Textbox(label="Username")
232
+ password = gr.Textbox(label="Password", type="password")
233
+ login_btn = gr.Button("Login")
234
+ login_status = gr.Textbox(label="Status")
235
+ with gr.Group(visible=False) as dashboard:
236
+ title = gr.Textbox(label="Title")
237
+ content = gr.Textbox(label="Content", lines=3)
238
+ course = gr.Dropdown(choices=school_service.courses[1:], label="Course")
239
+ priority = gr.Radio(["low", "normal", "high"], label="Priority")
240
+ post_btn = gr.Button("Post")
241
+ post_result = gr.Textbox(label="Result")
242
+ logout_btn = gr.Button("Logout")
243
+
244
+ login_btn.click(fn=admin_login, inputs=[username, password], outputs=[login_group, dashboard, login_status, post_btn])
245
+ logout_btn.click(fn=admin_logout, inputs=None, outputs=[login_group, dashboard, login_status, post_result])
246
+ post_btn.click(fn=post_announcement, inputs=[title, content, course, priority], outputs=post_result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
 
 
 
 
 
 
248
 
 
249
  if __name__ == "__main__":
250
  demo.launch()