Yoans commited on
Commit
e2b470a
·
verified ·
1 Parent(s): 117cbb8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +96 -348
app.py CHANGED
@@ -1,11 +1,7 @@
1
  # app.py
2
  # ThinkPal – Hugging Face Space (Gradio)
3
 
4
- import os
5
- import json
6
- import uuid
7
- import re
8
- import unicodedata
9
  from difflib import get_close_matches, SequenceMatcher
10
 
11
  import gradio as gr
@@ -20,7 +16,7 @@ except Exception:
20
  # Config
21
  # -----------------------------
22
  DATA_FILE = "student_profiles.json"
23
- GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-1.5-pro")
24
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
25
 
26
  if GEMINI_API_KEY and genai:
@@ -29,129 +25,91 @@ if GEMINI_API_KEY and genai:
29
  else:
30
  _gemini_model = None # fallback to local simulated responses
31
 
32
-
33
  # -----------------------------
34
  # Storage helpers (JSON)
35
  # -----------------------------
36
- def load_students():
37
  if not os.path.exists(DATA_FILE):
38
  return {}
39
  with open(DATA_FILE, "r", encoding="utf-8") as f:
40
  return json.load(f)
41
 
42
-
43
- def save_students(data):
44
  with open(DATA_FILE, "w", encoding="utf-8") as f:
45
  json.dump(data, f, indent=2, ensure_ascii=False)
46
 
47
-
48
- def list_student_ids():
49
  return sorted(load_students().keys())
50
 
51
-
52
- def get_student(student_id):
53
  return load_students().get(student_id)
54
 
55
-
56
- def add_student(data):
57
  students = load_students()
58
- # unique random id (short)
59
- new_id = f"student_{uuid.uuid4().hex[:8]}"
60
  students[new_id] = data
61
  save_students(students)
62
  return new_id
63
 
64
-
65
- def update_student(student_id, updates):
66
  students = load_students()
67
  if student_id not in students:
68
  return f"❌ {student_id} not found."
69
- # merge shallowly (keep structure)
70
  for k, v in updates.items():
71
  if v not in [None, "", []]:
72
  students[student_id][k] = v
73
  save_students(students)
74
  return f"✅ {student_id} updated."
75
 
76
-
77
  # -----------------------------
78
- # FAQs (Arabic + English) + fuzzy match
79
  # -----------------------------
80
  FAQS = {
81
- "إيه هو ThinkPal؟": "ThinkPal منصة بتساعدك تعرف نفسك أكتر وتتعلم بالطريقة اللي تناسبك وكمان تديك خطة تعليمية واضحة خطوة بخطوة علشان توصل لهدفك.",
82
- "هل الموقع للثانوي ولا الجامعة؟": "ThinkPal معمول بالأساس لطلاب الجامعة لكن أي طالب حابب يطور من نفسه أو يكتشف طريقه يقدر يستخدمه.",
83
- "أنا ليه أجاوب على الأسئلة أول ما أدخل؟": "الأسئلة بتساعدنا نفهم طريقتك وشخصيتك علشان نعمل خطة مناسبة ليك.",
84
- "الأسئلة اللي بتظهر صعبة شوية أجاوب إزاي؟": "مش امتحان ومفيش صح وغلط. جاوب بطريقتك.",
85
- "الخطة التعليمية بتكون إيه بالظبط؟": "خطوات من مبتدئ لمتقدم + مصادر موثوقة وتمارين عملية.",
86
- "هل المصادر كلها مجانية؟": "معظمها مجاني وفيه اختيارات مدفوعة بنرشحها أحيانًا.",
87
  "هل لازم أمشي بالخطة زي ما هي؟": "الخطة مرنة وإنت اللي بتحدد السرعة.",
88
- "يعني ThinkPal بديل للدروس أو الكورسات؟": "مش بديل—هو دليل/مرشد بيوضح الطريق.",
89
- "هل في متابعة لتقدمي؟": "أيوة، Dashboard يوضح الإنجازات والاختبارات والتقييم والـBadges.",
90
- "إيه هي Insights اللي بتظهرلي؟": "ملاحظات عملية عن نقاط القوة والتحسين بناءً على بياناتك.",
91
- "هل فيه تواصل مع طلاب تانيين؟": "أيوة، فيه مجتمع داخلي ومُرشدين (Mentors).",
92
- "هل المنصة بتركز على الدراسة بس؟": "لا، كمان على تطوير الشخصية والمهارات وتحديد المستقبل.",
93
- "الخصوصية آمنة؟": "أيوة، بياناتك ��حمية ومقفولة عليك.",
94
- "لو وقفت في نص الطريق؟": "ولا يهمك—تقدر ترجع في أي وقت وتكمل من نفس مكانك.",
95
-
96
- # English mirrors
97
- "What is ThinkPal?": "ThinkPal helps you understand yourself and learn your way, with a step-by-step plan.",
98
- "Is it for high school or university?": "Mainly for university students, but anyone can use it.",
99
- "Why do I answer questions at the start?": "They tailor your plan to your style and goals.",
100
- "Are the questions hard?": "No right/wrong; answer naturally.",
101
- "What is the learning plan exactly?": "Phased from beginner to advanced, with trusted resources and practice.",
102
  "Are all resources free?": "Many are free; some paid options may be suggested.",
103
  "Do I have to follow the plan exactly?": "It's flexible; you control the pace.",
104
  "Is ThinkPal a replacement for courses?": "It's a guide, not a replacement.",
105
- "Do you track my progress?": "Yes—dashboard, tests, ratings, and badges.",
106
- "What are the Insights?": "Actionable notes on strengths and areas to improve.",
107
- "Can I connect with other students?": "Yes—community + mentors.",
108
- "Is my data private?": "Yes—your data is protected.",
109
  "What if I stop halfway?": "No problem, resume anytime."
110
  }
111
 
112
- FAQ_SYNONYMS = [
113
- "لو وقفت", "نص الطريق", "رجوع", "resume", "stop halfway", "come back later",
114
- "ايه هو", "what is thinkpal", "plan exactly", "sources free", "privacy safe",
115
- "community", "mentors", "progress tracking", "insights meaning"
116
- ]
117
-
118
-
119
- def _normalize(text):
120
- t = (text or "").lower().strip()
121
- # remove diacritics / normalize Arabic
122
  t = "".join(c for c in unicodedata.normalize("NFD", t) if unicodedata.category(c) != "Mn")
123
  t = re.sub(r"[^\w\s\u0600-\u06FF]", " ", t)
124
- t = re.sub(r"\s+", " ", t)
125
- return t
126
-
127
 
128
- def find_faq_answer(user_input, cutoff=0.6):
129
  if not user_input:
130
  return None
131
  ui = _normalize(user_input)
132
-
133
- # exact/near match on known questions
134
  questions = list(FAQS.keys())
135
  norm_qs = {_normalize(q): q for q in questions}
136
-
137
- # try close matches against normalized questions
138
  match_keys = get_close_matches(ui, list(norm_qs.keys()), n=1, cutoff=cutoff)
139
  if match_keys:
140
  return FAQS[norm_qs[match_keys[0]]]
141
-
142
- # fuzzy against synonyms -> pick best FAQ by similarity
143
- candidates = questions + FAQ_SYNONYMS
144
- best_q = max(candidates, key=lambda q: SequenceMatcher(None, ui, _normalize(q)).ratio())
145
- if SequenceMatcher(None, ui, _normalize(best_q)).ratio() >= cutoff:
146
- # map a synonym to a reasonable FAQ (heuristic)
147
- if any(k in ui for k in ["نص الطريق", "لو وقفت", "stop halfway", "resume"]):
148
- # key exists in FAQS
149
- return FAQS.get("لو وقفت في نص الطريق؟") or FAQS.get("لو وقفت في نص الطريق") or FAQS.get("What if I stop halfway?")
150
- if best_q in FAQS:
151
- return FAQS[best_q]
152
  return None
153
 
154
-
155
  # -----------------------------
156
  # Prompting
157
  # -----------------------------
@@ -161,341 +119,131 @@ Generate a personalized learning roadmap for a student with phases:
161
  - Intermediate
162
  - Advanced
163
  - Challenge
164
-
165
- Consider: learning style, academic progress, personality, interests, goals, level,
166
- preferred methods, IQ, EQ, decision-making style, motivation, and study environment.
167
-
168
- Output strictly in plain text (no bold, no emojis, no AI disclaimers).
169
- Use clear headings and numbered lists.
170
-
171
- Sections required:
172
- 1) Current Status
173
- 2) Goals
174
- 3) Recommended Resources & Activities (by Phase)
175
- 4) Milestones (by Phase)
176
  """
177
 
178
-
179
- def _compose_profile(student):
180
- mapping = [
181
- ("learning_style", "Learning Style"),
182
- ("academic_progress", "Academic Progress"),
183
- ("personality", "Personality"),
184
- ("interests", "Interests"),
185
- ("goals", "Goals"),
186
- ("level", "Level"),
187
- ("preferred_methods", "Preferred Methods"),
188
- ("iq_level", "IQ Level"),
189
- ("eq_level", "EQ Level"),
190
- ("decision_making_style", "Decision-Making Style"),
191
- ("motivation_level", "Motivation Level"),
192
- ("preferred_study_environment", "Preferred Study Environment"),
193
- ("community_groups", "Community Groups"),
194
- ]
195
  parts = []
196
- for key, label in mapping:
197
- val = student.get(key) if student else None
198
- if isinstance(val, list):
199
- val = ", ".join(val)
200
- if val:
201
- parts.append(f"{label}: {val}")
202
- return " | ".join(parts) if parts else "No student data provided."
203
-
204
-
205
- def get_gemini_response(query, student=None):
206
  profile = _compose_profile(student or {})
207
- prompt = f"""Student Profile: {profile}
208
-
209
- Task:
210
- {query}
211
-
212
- Formatting:
213
- - Plain text only (no **, no markdown emphasis, no emojis, no disclaimers).
214
- - Use short headings and numbered lists where appropriate.
215
- - Keep it concise and practical."""
216
-
217
  if _gemini_model:
218
  try:
219
  resp = _gemini_model.generate_content(prompt)
220
- return getattr(resp, "text", "").strip() or "(Empty response)"
221
  except Exception as e:
222
- return f"(Gemini error fallback) {str(e)[:160]}"
223
- # fallback (no API key)
224
- # keep simulated short to avoid huge outputs in UI
225
  return f"(Simulated) {prompt[:400]}..."
226
 
227
-
228
- def generate_ai_insights(student):
229
  profile = _compose_profile(student or {})
230
- prompt = f"""Analyze the following student profile and provide insights:
231
- {profile}
232
-
233
- Requirements:
234
- - Plain text only (no **, no emojis, no disclaimers)
235
- - Bullet points for strengths and areas to improve
236
- - 5–8 lines max
237
- """
238
  if _gemini_model:
239
  try:
240
- resp = _gemini_model.generate_content(prompt)
241
- return getattr(resp, "text", "").strip() or "(Empty response)"
242
  except Exception as e:
243
- return f"(Gemini error fallback) {str(e)[:160]}"
244
  return f"(Simulated insights) {profile}"
245
 
246
-
247
  # -----------------------------
248
  # Chat logic
249
  # -----------------------------
250
- def chat(student_id, message):
251
- """Returns (roadmap_text, insights_text, chatbot_response_text)"""
252
  roadmap, insights, reply = "", "", ""
253
  student = get_student(student_id)
254
-
255
  if not student:
256
- return ("❌ Student not found. Add a profile first.", "", "")
257
-
258
  m = (message or "").strip()
259
  if not m:
260
  return ("", "", "Please enter a message.")
261
-
262
- low = m.lower()
263
- if low == "roadmap":
264
- roadmap = get_gemini_response(ROADMAP_QUERY, student) or "Could not generate roadmap."
265
- elif low == "insights":
266
- insights = generate_ai_insights(student) or "Could not generate insights."
267
  else:
268
- faq = find_faq_answer(m, cutoff=0.58)
269
- if faq:
270
- reply = f"{faq}"
271
- else:
272
- reply = get_gemini_response(m, student) or "Sorry, I couldn't process that."
273
  return roadmap, insights, reply
274
 
275
-
276
  # -----------------------------
277
- # Add / Update helpers for UI
278
  # -----------------------------
279
- ALL_FIELDS = [
280
- "learning_style", "academic_progress", "personality", "interests", "goals",
281
- "level", "preferred_methods", "iq_level", "eq_level",
282
- "decision_making_style", "motivation_level",
283
- "preferred_study_environment", "community_groups"
284
- ]
285
-
286
-
287
- def create_student(
288
- learning_style, academic_progress, personality, interests, goals, level,
289
- preferred_methods, iq_level, eq_level, decision_making_style,
290
- motivation_level, preferred_study_environment, community_groups
291
- ):
292
- data = {
293
- "learning_style": learning_style,
294
- "academic_progress": academic_progress,
295
- "personality": personality,
296
- "interests": interests,
297
- "goals": goals,
298
- "level": level,
299
- "preferred_methods": [s.strip() for s in (preferred_methods or "").split(",") if s.strip()],
300
- "iq_level": iq_level,
301
- "eq_level": eq_level,
302
- "decision_making_style": decision_making_style,
303
- "motivation_level": motivation_level,
304
- "preferred_study_environment": preferred_study_environment,
305
- "community_groups": [s.strip() for s in (community_groups or "").split(",") if s.strip()],
306
- }
307
  new_id = add_student(data)
308
- # return status, preview json, new dropdown choices, new default
309
  return (
310
  f"🎉 Created {new_id}",
311
  json.dumps(data, ensure_ascii=False, indent=2),
312
  gr.Dropdown.update(choices=list_student_ids(), value=new_id)
313
  )
314
 
315
-
316
- def load_student_to_form(student_id):
317
  s = get_student(student_id)
318
- if not s:
319
- # return empties
320
- return [""] * 13
321
- return [
322
- s.get("learning_style", ""),
323
- s.get("academic_progress", ""),
324
- s.get("personality", ""),
325
- s.get("interests", ""),
326
- s.get("goals", ""),
327
- s.get("level", ""),
328
- ", ".join(s.get("preferred_methods", [])),
329
- s.get("iq_level", ""),
330
- s.get("eq_level", ""),
331
- s.get("decision_making_style", ""),
332
- s.get("motivation_level", ""),
333
- s.get("preferred_study_environment", ""),
334
- ", ".join(s.get("community_groups", [])),
335
- ]
336
-
337
 
338
- def apply_update(
339
- student_id,
340
- learning_style, academic_progress, personality, interests, goals, level,
341
- preferred_methods, iq_level, eq_level, decision_making_style,
342
- motivation_level, preferred_study_environment, community_groups
343
- ):
344
- updates = {
345
- "learning_style": learning_style,
346
- "academic_progress": academic_progress,
347
- "personality": personality,
348
- "interests": interests,
349
- "goals": goals,
350
- "level": level,
351
- "preferred_methods": [s.strip() for s in (preferred_methods or "").split(",") if s.strip()],
352
- "iq_level": iq_level,
353
- "eq_level": eq_level,
354
- "decision_making_style": decision_making_style,
355
- "motivation_level": motivation_level,
356
- "preferred_study_environment": preferred_study_environment,
357
- "community_groups": [s.strip() for s in (community_groups or "").split(",") if s.strip()],
358
- }
359
  msg = update_student(student_id, updates)
360
- preview = json.dumps(get_student(student_id) or {}, ensure_ascii=False, indent=2)
361
- return msg, preview
362
 
363
  # -----------------------------
364
- # UI Theme
365
  # -----------------------------
366
- THEME = gr.themes.Soft(
367
- primary_hue="indigo",
368
- secondary_hue="cyan"
369
- ).set(
370
- body_background_fill="#0b0e1a",
371
- body_text_color="#e0e0e0",
372
- block_background_fill="#0f1220",
373
- block_border_color="#2a2f4a",
374
- block_label_text_color="#e8eaf6",
375
- input_background_fill="#14182b",
376
- input_border_color="#2a2f4a",
377
- input_text_color="#e0e0e0",
378
- button_primary_background_fill="linear-gradient(90deg, #6C63FF, #00BCD4)",
379
- button_primary_text_color="#ffffff",
380
- )
381
 
382
- with gr.Blocks(theme=THEME, css="""
383
  #header {padding: 24px 0 8px;}
384
  #header h1 {margin:0; font-size: 2rem;}
385
  .small {opacity:.85; font-size:.9rem}
386
  .card {border:1px solid #2a2f4a; border-radius:12px; padding:12px;}
387
- """) as demo:
388
- gr.HTML("""
389
- <div id="header">
390
- <h1>🎓 ThinkPal – Personalized Learning Assistant</h1>
391
- <div class="small">Roadmaps, insights, and a friendly FAQ — tailored to each student.</div>
392
- </div>
393
- """)
394
 
395
  with gr.Tab("💬 Chat"):
396
  with gr.Row():
397
- student_dd = gr.Dropdown(
398
- label="Select Student",
399
- choices=list_student_ids() or [],
400
- value=(list_student_ids() or [None])[0] if list_student_ids() else None
401
- )
402
- user_msg = gr.Textbox(
403
- label="Your Message",
404
- placeholder="Type: roadmap, insights, or any question (Arabic/English)",
405
- )
406
  ask_btn = gr.Button("Ask", variant="primary")
407
  with gr.Row():
408
  roadmap_out = gr.Textbox(label="Roadmap", lines=14, elem_classes=["card"])
409
  insights_out = gr.Textbox(label="Insights", lines=10, elem_classes=["card"])
410
  chatbot_out = gr.Textbox(label="Response", lines=6, elem_classes=["card"])
411
-
412
- ask_btn.click(
413
- fn=chat,
414
- inputs=[student_dd, user_msg],
415
- outputs=[roadmap_out, insights_out, chatbot_out]
416
- )
417
 
418
  with gr.Tab("➕ Add Student"):
419
- gr.Markdown("Fill the form. Lists accept comma-separated values (e.g., `online videos, interactive exercises`).")
420
- with gr.Row():
421
- learning_style = gr.Textbox(label="Learning Style")
422
- academic_progress = gr.Textbox(label="Academic Progress")
423
- personality = gr.Textbox(label="Personality")
424
- with gr.Row():
425
- interests = gr.Textbox(label="Interests")
426
- goals = gr.Textbox(label="Goals")
427
- level = gr.Textbox(label="Level")
428
- preferred_methods = gr.Textbox(label="Preferred Methods (comma-separated)")
429
- with gr.Row():
430
- iq_level = gr.Textbox(label="IQ Level")
431
- eq_level = gr.Textbox(label="EQ Level")
432
- decision_style = gr.Textbox(label="Decision-Making Style")
433
- with gr.Row():
434
- motivation_level = gr.Textbox(label="Motivation Level")
435
- study_env = gr.Textbox(label="Preferred Study Environment")
436
- community_groups = gr.Textbox(label="Community Groups (comma-separated)")
437
-
438
  create_btn = gr.Button("Create Profile", variant="primary")
439
- status_new = gr.Textbox(label="Status", interactive=False)
440
- preview_new = gr.Textbox(label="Saved Profile Preview", lines=10)
441
-
442
- # also update the chat dropdown after create
443
- create_btn.click(
444
- fn=create_student,
445
- inputs=[learning_style, academic_progress, personality, interests, goals, level,
446
- preferred_methods, iq_level, eq_level, decision_style,
447
- motivation_level, study_env, community_groups],
448
- outputs=[status_new, preview_new, student_dd]
449
- )
450
 
451
  with gr.Tab("✏️ Update Student"):
452
- gr.Markdown("Pick a student, load their data, edit fields you want, then save.")
453
  target_id = gr.Dropdown(label="Student", choices=list_student_ids() or [])
454
  load_btn = gr.Button("Load Profile")
455
- with gr.Row():
456
- u_learning_style = gr.Textbox(label="Learning Style")
457
- u_academic_progress = gr.Textbox(label="Academic Progress")
458
- u_personality = gr.Textbox(label="Personality")
459
- with gr.Row():
460
- u_interests = gr.Textbox(label="Interests")
461
- u_goals = gr.Textbox(label="Goals")
462
- u_level = gr.Textbox(label="Level")
463
- u_preferred_methods = gr.Textbox(label="Preferred Methods (comma-separated)")
464
- with gr.Row():
465
- u_iq_level = gr.Textbox(label="IQ Level")
466
- u_eq_level = gr.Textbox(label="EQ Level")
467
- u_decision_style = gr.Textbox(label="Decision-Making Style")
468
- with gr.Row():
469
- u_motivation_level = gr.Textbox(label="Motivation Level")
470
- u_study_env = gr.Textbox(label="Preferred Study Environment")
471
- u_community_groups = gr.Textbox(label="Community Groups (comma-separated)")
472
-
473
  save_btn = gr.Button("Save Changes", variant="primary")
474
- status_upd = gr.Textbox(label="Status", interactive=False)
475
- preview_upd = gr.Textbox(label="Updated Profile Preview", lines=10)
476
-
477
- load_btn.click(
478
- fn=load_student_to_form,
479
- inputs=[target_id],
480
- outputs=[u_learning_style, u_academic_progress, u_personality,
481
- u_interests, u_goals, u_level, u_preferred_methods,
482
- u_iq_level, u_eq_level, u_decision_style, u_motivation_level,
483
- u_study_env, u_u_community_groups] if False else
484
- [u_learning_style, u_academic_progress, u_personality,
485
- u_interests, u_goals, u_level, u_preferred_methods,
486
- u_iq_level, u_eq_level, u_decision_style, u_motivation_level,
487
- u_study_env, u_community_groups]
488
- )
489
-
490
- save_btn.click(
491
- fn=apply_update,
492
- inputs=[target_id, u_learning_style, u_academic_progress, u_personality,
493
- u_interests, u_goals, u_level, u_preferred_methods,
494
- u_iq_level, u_eq_level, u_decision_style, u_motivation_level,
495
- u_study_env, u_community_groups],
496
- outputs=[status_upd, preview_upd]
497
- )
498
 
499
  if __name__ == "__main__":
500
- # Space-friendly: server will bind host/port automatically
501
  demo.launch()
 
1
  # app.py
2
  # ThinkPal – Hugging Face Space (Gradio)
3
 
4
+ import os, json, uuid, re, unicodedata
 
 
 
 
5
  from difflib import get_close_matches, SequenceMatcher
6
 
7
  import gradio as gr
 
16
  # Config
17
  # -----------------------------
18
  DATA_FILE = "student_profiles.json"
19
+ GEMINI_MODEL = os.getenv("GEMINI_MODEL", "models/gemma-3n-e2b-it")
20
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
21
 
22
  if GEMINI_API_KEY and genai:
 
25
  else:
26
  _gemini_model = None # fallback to local simulated responses
27
 
 
28
  # -----------------------------
29
  # Storage helpers (JSON)
30
  # -----------------------------
31
+ def load_students() -> dict:
32
  if not os.path.exists(DATA_FILE):
33
  return {}
34
  with open(DATA_FILE, "r", encoding="utf-8") as f:
35
  return json.load(f)
36
 
37
+ def save_students(data: dict) -> None:
 
38
  with open(DATA_FILE, "w", encoding="utf-8") as f:
39
  json.dump(data, f, indent=2, ensure_ascii=False)
40
 
41
+ def list_student_ids() -> list:
 
42
  return sorted(load_students().keys())
43
 
44
+ def get_student(student_id: str) -> dict | None:
 
45
  return load_students().get(student_id)
46
 
47
+ def add_student(data: dict) -> str:
 
48
  students = load_students()
49
+ new_id = f"student_{uuid.uuid4().hex[:8]}" # unique random id
 
50
  students[new_id] = data
51
  save_students(students)
52
  return new_id
53
 
54
+ def update_student(student_id: str, updates: dict) -> str:
 
55
  students = load_students()
56
  if student_id not in students:
57
  return f"❌ {student_id} not found."
 
58
  for k, v in updates.items():
59
  if v not in [None, "", []]:
60
  students[student_id][k] = v
61
  save_students(students)
62
  return f"✅ {student_id} updated."
63
 
 
64
  # -----------------------------
65
+ # FAQs (Arabic + English)
66
  # -----------------------------
67
  FAQS = {
68
+ "إيه هو ThinkPal؟": "ThinkPal منصة بتساعدك تعرف نفسك أكتر وتتعلم بالطريقة اللي تناسبك...",
69
+ "هل الموقع للثانوي ولا الجامعة؟": "ThinkPal معمول بالأساس لطلاب الجامعة...",
70
+ "أنا ليه أجاوب على الأسئلة أول ما أدخل؟": "الأسئلة بتساعدنا نفهم طريقتك وشخصيتك...",
71
+ "الخطة التعليمية بتكون إيه بالظبط؟": "خطوات من مبتدئ لمتقدم + مصادر موثوقة...",
72
+ "هل المصادر كلها مجانية؟": "معظمها مجاني وفيه اختيارات مدفوعة...",
 
73
  "هل لازم أمشي بالخطة زي ما هي؟": "الخطة مرنة وإنت اللي بتحدد السرعة.",
74
+ "يعني ThinkPal بديل للدروس أو الكورسات؟": "مش بديل هو مرشد بيوضح الطريق.",
75
+ "هل في متابعة لتقدمي؟": "أيوة، Dashboard يوضح الإنجازات والاختبارات.",
76
+ "إيه هي Insights اللي بتظهرلي؟": "ملاحظات عملية عن نقاط القوة والتحسين.",
77
+ "هل فيه تواصل مع طلاب تانيين؟": "أيوة، فيه مجتمع داخلي ومُرشدين.",
78
+ "هل المنصة بتركز على الدراسة بس؟": "لا، كمان على تطوير الشخصية والمهارات.",
79
+ "الخصوصية آمنة؟": "أيوة، بياناتك محمية.",
80
+ "لو وقفت في نص الطريق؟": "ولا يهمك—تقدر ترجع في أي وقت وتكمل.",
81
+
82
+ "What is ThinkPal?": "ThinkPal helps you understand yourself...",
83
+ "Is it for high school or university?": "Mainly for university students...",
84
+ "Why do I answer questions at the start?": "They tailor your plan...",
85
+ "What is the learning plan exactly?": "Phased from beginner to advanced...",
 
 
86
  "Are all resources free?": "Many are free; some paid options may be suggested.",
87
  "Do I have to follow the plan exactly?": "It's flexible; you control the pace.",
88
  "Is ThinkPal a replacement for courses?": "It's a guide, not a replacement.",
89
+ "Do you track my progress?": "Yes dashboard, tests, badges.",
90
+ "What are the Insights?": "Notes on strengths and areas to improve.",
91
+ "Can I connect with other students?": "Yes community + mentors.",
92
+ "Is my data private?": "Yes your data is protected.",
93
  "What if I stop halfway?": "No problem, resume anytime."
94
  }
95
 
96
+ def _normalize(text: str) -> str:
97
+ t = text.lower().strip()
 
 
 
 
 
 
 
 
98
  t = "".join(c for c in unicodedata.normalize("NFD", t) if unicodedata.category(c) != "Mn")
99
  t = re.sub(r"[^\w\s\u0600-\u06FF]", " ", t)
100
+ return re.sub(r"\s+", " ", t)
 
 
101
 
102
+ def find_faq_answer(user_input: str, cutoff: float = 0.6) -> str | None:
103
  if not user_input:
104
  return None
105
  ui = _normalize(user_input)
 
 
106
  questions = list(FAQS.keys())
107
  norm_qs = {_normalize(q): q for q in questions}
 
 
108
  match_keys = get_close_matches(ui, list(norm_qs.keys()), n=1, cutoff=cutoff)
109
  if match_keys:
110
  return FAQS[norm_qs[match_keys[0]]]
 
 
 
 
 
 
 
 
 
 
 
111
  return None
112
 
 
113
  # -----------------------------
114
  # Prompting
115
  # -----------------------------
 
119
  - Intermediate
120
  - Advanced
121
  - Challenge
 
 
 
 
 
 
 
 
 
 
 
 
122
  """
123
 
124
+ def _compose_profile(student: dict) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  parts = []
126
+ for k, v in student.items():
127
+ if isinstance(v, list):
128
+ v = ", ".join(v)
129
+ if v:
130
+ parts.append(f"{k}: {v}")
131
+ return " | ".join(parts) if parts else "No data"
132
+
133
+ def get_gemini_response(query: str, student: dict | None = None) -> str:
 
 
134
  profile = _compose_profile(student or {})
135
+ prompt = f"Student Profile: {profile}\nTask:\n{query}"
 
 
 
 
 
 
 
 
 
136
  if _gemini_model:
137
  try:
138
  resp = _gemini_model.generate_content(prompt)
139
+ return getattr(resp, "text", "").strip()
140
  except Exception as e:
141
+ return f"(Gemini error) {e}"
 
 
142
  return f"(Simulated) {prompt[:400]}..."
143
 
144
+ def generate_ai_insights(student: dict) -> str:
 
145
  profile = _compose_profile(student or {})
 
 
 
 
 
 
 
 
146
  if _gemini_model:
147
  try:
148
+ resp = _gemini_model.generate_content(f"Insights for: {profile}")
149
+ return getattr(resp, "text", "").strip()
150
  except Exception as e:
151
+ return f"(Gemini error) {e}"
152
  return f"(Simulated insights) {profile}"
153
 
 
154
  # -----------------------------
155
  # Chat logic
156
  # -----------------------------
157
+ def chat(student_id: str, message: str) -> tuple[str, str, str]:
 
158
  roadmap, insights, reply = "", "", ""
159
  student = get_student(student_id)
 
160
  if not student:
161
+ return ("❌ Student not found.", "", "")
 
162
  m = (message or "").strip()
163
  if not m:
164
  return ("", "", "Please enter a message.")
165
+ if m.lower() == "roadmap":
166
+ roadmap = get_gemini_response(ROADMAP_QUERY, student)
167
+ elif m.lower() == "insights":
168
+ insights = generate_ai_insights(student)
 
 
169
  else:
170
+ faq = find_faq_answer(m)
171
+ reply = faq if faq else get_gemini_response(m, student)
 
 
 
172
  return roadmap, insights, reply
173
 
 
174
  # -----------------------------
175
+ # Add / Update helpers
176
  # -----------------------------
177
+ def create_student(**kwargs):
178
+ data = {k: (v.split(",") if "methods" in k or "groups" in k else v) for k, v in kwargs.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  new_id = add_student(data)
 
180
  return (
181
  f"🎉 Created {new_id}",
182
  json.dumps(data, ensure_ascii=False, indent=2),
183
  gr.Dropdown.update(choices=list_student_ids(), value=new_id)
184
  )
185
 
186
+ def load_student_to_form(student_id: str):
 
187
  s = get_student(student_id)
188
+ if not s: return [""] * 13
189
+ return [", ".join(v) if isinstance(v, list) else v for v in s.values()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ def apply_update(student_id, **kwargs):
192
+ updates = {k: (v.split(",") if "methods" in k or "groups" in k else v) for k, v in kwargs.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  msg = update_student(student_id, updates)
194
+ return msg, json.dumps(get_student(student_id) or {}, ensure_ascii=False, indent=2)
 
195
 
196
  # -----------------------------
197
+ # UI
198
  # -----------------------------
199
+ THEME = gr.themes.Soft(primary_hue="indigo", secondary_hue="cyan")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ CUSTOM_CSS = """
202
  #header {padding: 24px 0 8px;}
203
  #header h1 {margin:0; font-size: 2rem;}
204
  .small {opacity:.85; font-size:.9rem}
205
  .card {border:1px solid #2a2f4a; border-radius:12px; padding:12px;}
206
+ .gradio-container {background-color: #0b0e1a !important; color: #e0e0e0 !important;}
207
+ textarea, input, select {background-color: #14182b !important; border: 1px solid #2a2f4a !important; color: #e0e0e0 !important;}
208
+ button.primary {background: linear-gradient(90deg, #6C63FF, #00BCD4) !important; color: #fff !important;}
209
+ """
210
+
211
+ with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
212
+ gr.HTML("<div id='header'><h1>🎓 ThinkPal – Personalized Learning Assistant</h1></div>")
213
 
214
  with gr.Tab("💬 Chat"):
215
  with gr.Row():
216
+ student_dd = gr.Dropdown(label="Select Student", choices=list_student_ids() or [])
217
+ user_msg = gr.Textbox(label="Message", placeholder="Type roadmap, insights, or a question")
 
 
 
 
 
 
 
218
  ask_btn = gr.Button("Ask", variant="primary")
219
  with gr.Row():
220
  roadmap_out = gr.Textbox(label="Roadmap", lines=14, elem_classes=["card"])
221
  insights_out = gr.Textbox(label="Insights", lines=10, elem_classes=["card"])
222
  chatbot_out = gr.Textbox(label="Response", lines=6, elem_classes=["card"])
223
+ ask_btn.click(fn=chat, inputs=[student_dd, user_msg], outputs=[roadmap_out, insights_out, chatbot_out])
 
 
 
 
 
224
 
225
  with gr.Tab("➕ Add Student"):
226
+ fields = {k: gr.Textbox(label=k.replace("_", " ").title()) for k in
227
+ ["learning_style","academic_progress","personality","interests","goals","level",
228
+ "preferred_methods","iq_level","eq_level","decision_making_style",
229
+ "motivation_level","preferred_study_environment","community_groups"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  create_btn = gr.Button("Create Profile", variant="primary")
231
+ status_new = gr.Textbox(label="Status")
232
+ preview_new = gr.Textbox(label="Saved Profile", lines=10)
233
+ create_btn.click(fn=create_student, inputs=list(fields.values()), outputs=[status_new, preview_new, student_dd])
 
 
 
 
 
 
 
 
234
 
235
  with gr.Tab("✏️ Update Student"):
 
236
  target_id = gr.Dropdown(label="Student", choices=list_student_ids() or [])
237
  load_btn = gr.Button("Load Profile")
238
+ upd_fields = {k: gr.Textbox(label=k.replace("_", " ").title()) for k in
239
+ ["learning_style","academic_progress","personality","interests","goals","level",
240
+ "preferred_methods","iq_level","eq_level","decision_making_style",
241
+ "motivation_level","preferred_study_environment","community_groups"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  save_btn = gr.Button("Save Changes", variant="primary")
243
+ status_upd = gr.Textbox(label="Status")
244
+ preview_upd = gr.Textbox(label="Updated Profile", lines=10)
245
+ load_btn.click(fn=load_student_to_form, inputs=[target_id], outputs=list(upd_fields.values()))
246
+ save_btn.click(fn=apply_update, inputs=[target_id]+list(upd_fields.values()), outputs=[status_upd, preview_upd])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  if __name__ == "__main__":
 
249
  demo.launch()