Yoans commited on
Commit
a36a202
·
verified ·
1 Parent(s): 3204807

Update app.py

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