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

import os, json, uuid, re, unicodedata
from difflib import get_close_matches

import gradio as gr

# Optional Gemini (works if GEMINI_API_KEY set in HF Space secrets)
try:
    import google.generativeai as genai
except Exception:
    genai = None

# -----------------------------
# Config
# -----------------------------

DATA_FILE = "student_profiles.json"
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "models/gemma-3n-e2b-it")
GEMINI_API_KEY = os.getenv("Gemini_API_KEY")

if GEMINI_API_KEY and genai:
    genai.configure(api_key=GEMINI_API_KEY)
    _gemini_model = genai.GenerativeModel(GEMINI_MODEL)
else:
    _gemini_model = None  # fallback to local simulated responses

# -----------------------------
# Storage helpers (JSON)
# -----------------------------
def load_students() -> dict:
    if not os.path.exists(DATA_FILE):
        return {}
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)

def save_students(data: dict) -> None:
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)

def list_student_ids() -> list:
    return sorted(load_students().keys())

def get_student(student_id: str) -> dict | None:
    return load_students().get(student_id)

def add_student(data: dict) -> str:
    students = load_students()
    new_id = f"student_{uuid.uuid4().hex[:8]}"  # unique random id
    students[new_id] = data
    save_students(students)
    return new_id

def update_student(student_id: str, updates: dict) -> str:
    students = load_students()
    if student_id not in students:
        return f"❌ {student_id} not found."
    for k, v in updates.items():
        if v not in [None, "", []]:
            students[student_id][k] = v
    save_students(students)
    return f"✅ {student_id} updated."

# -----------------------------
# FAQs (Arabic + English)
# -----------------------------
FAQS = {
    # Arabic
    "إيه هو ThinkPal؟": "ThinkPal منصة بتساعدك تعرف نفسك أكتر وتتعلم بالطريقة اللي تناسبك وكمان تديك خطة تعليمية واضحة خطوة بخطوة علشان توصل لهدفك.",
    "هل الموقع للثانوي ولا الجامعة؟": "ThinkPal معمول بالأساس لطلاب الجامعة لكن أي طالب حابب يطور من نفسه أو يكتشف طريقه يقدر يستخدمه.",
    "أنا ليه أجاوب على الأسئلة أول ما أدخل؟": "لأن الأسئلة دي بتساعد المنصة تفهم شخصيتك وطريقة تفكيرك وتعلمك ومن هنا تقدر تبني لك خطة تناسبك أنت بالذات.",
    "الأسئلة اللي بتظهر صعبة شوية أجاوب إزاي؟": "دا مش امتحان ومفيش إجابة صح أو غلط، جاوب بطريقتك وباللي يمثلك. كل إجابة بتقرب الصورة أكتر.",
    "الخطة التعليمية بتكون إيه بالظبط؟": "الخطة عبارة عن خطوات من مستوى مبتدئ لحد مستوى متقدم ومع كل خطوة هتلاقي مصادر موثوقة للتعلم وتدريبات تساعدك تطبق اللي بتتعلمه.",
    "هل المصادر كلها مجانية؟": "فيه مصادر كتير مجانية وفيه كمان مصادر مدفوعة بنرشحها أحيانًا علشان تبقى قدامك كل الاختيارات.",
    "هل لازم أمشي بالخطة زي ما هي؟": "الخطة معمولة علشان تسهل عليك لكن في الآخر إنت بتختار سرعة العملية التعليمية.",
    "يعني ThinkPal بديل للدروس أو الكورسات؟": "لأ ThinkPal مش بديل هو زي دليل أو صديق بيرتب لك الطريق ويقولك تبدأ منين وتروح فين.",
    "هل في متابعة لتقدمي؟": "أيوة عندك Dashboard شخصي يوضح إنجازاتك، الاختبارات اللي عملتها، تقييمك، Badges، وأي نقاط محتاجة تشتغل عليها.",
    "إيه هي Insights اللي بتظهرلي؟": "دي ملاحظات بتقولك إيه نقاط قوتك وإيه الحاجات اللي محتاجة تحسين وكمان نصايح عملية تساعدك تطور نفسك.",
    "هل فيه تواصل مع طلاب تانيين؟": "أيوة فيه مجتمع جوا المنصة تقدر تتكلم فيه مع طلبة زيك وكمان فيه Mentors يساعدوك لو محتاج.",
    "هل المنصة بتركز على الدراسة بس؟": "لأ ThinkPal بيساعدك في الدراسة وكمان في تطوير شخصيتك و مهاراتك وحتى في إنك تحدد وتختار مستقبلك بشكل أوضح.",
    "الخصوصية آمنة؟": "أكيد بياناتك محمية ومفيش حد يقدر يشوفها غيرك.",
    "لو وقفت في نص الطريق؟": "مفيش مشكلة، تقدر ترجع في أي وقت وتكمل من نفس المكان اللي وصلتله.",

    # English
    "What is ThinkPal?": "ThinkPal is a platform that helps you understand yourself better, learn in the way that suits you, and gives you a clear step-by-step learning plan to achieve your goals.",
    "Is the platform for high school or university students?": "ThinkPal is mainly designed for university students, but any student who wants to improve or discover their path can use it.",
    "Why should I answer the questions when I first join?": "These questions help the platform understand your personality, thinking, and learning style, so it can build a plan tailored to you.",
    "The questions seem difficult, how should I answer?": "This is not an exam, and there are no right or wrong answers. Just answer in your own way with what represents you.",
    "What exactly is the learning roadmap?": "It’s a step-by-step plan from beginner to advanced levels. Each step includes trusted resources and exercises to help you apply what you learn.",
    "Are all resources free?": "Many resources are free, but we sometimes recommend paid resources to give you all available options.",
    "Do I have to follow the roadmap exactly as it is?": "The roadmap is designed to make it easier for you, but in the end, you choose the pace of your learning.",
    "Is ThinkPal a replacement for lessons or courses?": "No, ThinkPal is not a replacement. It’s more like a guide or a friend that organizes your path and tells you where to start and where to go.",
    "Is my progress tracked?": "Yes, you have a personal dashboard that shows your achievements, completed tests, evaluations, badges, and areas you need to work on.",
    "What are the insights shown to me?": "They are notes that highlight your strengths, areas for improvement, and practical tips to help you develop.",
    "Can I connect with other students?": "Yes, there’s a community inside the platform where you can talk with other students and get support from mentors if needed.",
    "Does the platform focus only on studying?": "No, ThinkPal helps with studying, but also with personal growth, skills development, and even clarifying your future direction.",
    "Is my privacy safe?": "Of course, your data is protected and no one can see it except you.",
    "What if I stop halfway?": "No problem, you can return anytime and continue from where you left off.",
}

def _normalize(text: str) -> str:
    t = text.lower().strip()
    t = "".join(c for c in unicodedata.normalize("NFD", t) if unicodedata.category(c) != "Mn")
    t = re.sub(r"[^\w\s\u0600-\u06FF]", " ", t)
    return re.sub(r"\s+", " ", t)

def find_faq_answer(user_input: str, cutoff: float = 0.6) -> str | None:
    if not user_input:
        return None
    ui = _normalize(user_input)
    questions = list(FAQS.keys())
    norm_qs = {_normalize(q): q for q in questions}
    match_keys = get_close_matches(ui, list(norm_qs.keys()), n=1, cutoff=cutoff)
    if match_keys:
        return FAQS[norm_qs[match_keys[0]]]
    return None

# -----------------------------
# Prompting
# -----------------------------
ROADMAP_QUERY = """
Generate a personalized learning roadmap for a student, structured into distinct phases:
- Beginner
- Intermediate
- Advanced
- Challenge

When creating the roadmap, consider the student's:
- Learning style
- Academic progress
- Personality
- Interests
- Goals
- Current level
- Preferred learning methods
- IQ level
- EQ level
- Decision-making style
- Motivation level
- Preferred study environment

The roadmap should help the student achieve their stated goals based on their unique characteristics.

For each phase, suggest specific types of resources tailored to the student's profile, such as:
- Online courses
- Books
- Interactive tools
- Hands-on projects
- Community engagement opportunities

Formatting requirements:
- Use plain text only (no **bold**, no markdown, no emojis, no AI disclaimers).
- Organize information with numbered lists and subheadings.
- Keep the tone professional, concise, and human-like.

Finally, structure the roadmap into the following sections:
1. Current Status
2. Goals
3. Recommended Resources & Activities (by Phase)
4. Milestones (by Phase)
"""

def get_gemini_response(query: str, student_data: dict | None = None) -> str:
    try:
        if not genai:
            return f"(Simulated) {query[:400]}..."

        model = genai.GenerativeModel("models/gemma-3n-e2b-it")
        personalized_prompt = query

        if student_data:
            profile_parts = []
            for key, label in [
                ("learning_style", "Learning Style"),
                ("academic_progress", "Academic Progress"),
                ("personality", "Personality"),
                ("interests", "Interests"),
                ("goals", "Goals"),
                ("level", "Level"),
                ("preferred_methods", "Preferred Methods"),
                ("iq_level", "IQ Level"),
                ("eq_level", "EQ Level"),
                ("decision_making_style", "Decision-Making Style"),
                ("motivation_level", "Motivation Level"),
                ("preferred_study_environment", "Preferred Study Environment"),
                ("community_groups", "Community Groups"),
            ]:
                value = student_data.get(key)
                if isinstance(value, list):
                    value = ", ".join(value)
                if value:
                    profile_parts.append(f"{label}: {value}")

            student_profile = ", ".join(profile_parts) if profile_parts else "No student data provided."

            personalized_prompt = f"""
The student's profile is: {student_profile}.

Based on this profile, generate the following:

{query}

Formatting requirements:
- Use plain text only (no **bold**, no markdown, no emojis, no AI disclaimers).
- Organize information with numbered lists and subheadings.
- Keep the tone professional, concise, and human-like.
"""

        response = model.generate_content(personalized_prompt)
        return getattr(response, "text", "").strip() or "(Empty response)"

    except Exception as e:
        return f"(Gemini error fallback) {str(e)[:160]}"

def generate_ai_insights(student_data: dict) -> str:
    if not student_data:
        return "Student data not available for generating insights."

    profile_parts = []
    for key, label in [
        ("learning_style", "Learning Style"),
        ("academic_progress", "Academic Progress"),
        ("personality", "Personality"),
        ("interests", "Interests"),
        ("goals", "Goals"),
        ("level", "Level"),
        ("preferred_methods", "Preferred Methods"),
        ("iq_level", "IQ Level"),
        ("eq_level", "EQ Level"),
        ("decision_making_style", "Decision-Making Style"),
        ("motivation_level", "Motivation Level"),
        ("preferred_study_environment", "Preferred Study Environment"),
        ("community_groups", "Community Groups"),
    ]:
        value = student_data.get(key)
        if isinstance(value, list):
            value = ", ".join(value)
        if value:
            profile_parts.append(f"- {label}: {value}")

    student_profile = "\n".join(profile_parts) if profile_parts else "No detailed data provided."

    insights_prompt = f"""
Analyze the following student profile and provide AI-driven insights,
highlighting their strengths and potential areas for improvement.

Student Profile:
{student_profile}

Formatting requirements:
- Use plain text only (no **bold**, no markdown, no emojis, no AI disclaimers).
- Organize insights clearly with numbered or bulleted points.
- Keep the tone professional, concise, and human-like.
"""

    return get_gemini_response(insights_prompt, student_data)

# -----------------------------
# Chat logic
# -----------------------------
def chat(student_id: str, message: str) -> tuple[str, str, str]:
    roadmap, insights, reply = "", "", ""
    student = get_student(student_id)
    if not student:
        return ("❌ Student not found.", "", "")
    m = (message or "").strip()
    if not m:
        return ("", "", "Please enter a message.")
    if m.lower() == "roadmap":
        roadmap = get_gemini_response(ROADMAP_QUERY, student)
    elif m.lower() == "insights":
        insights = generate_ai_insights(student)
    else:
        faq = find_faq_answer(m)
        reply = faq if faq else get_gemini_response(m, student)
    return roadmap, insights, reply

# -----------------------------
# Add / Update helpers
# -----------------------------
FIELDS = [
    "learning_style","academic_progress","personality","interests","goals","level",
    "preferred_methods","iq_level","eq_level","decision_making_style",
    "motivation_level","preferred_study_environment","community_groups"
]

def create_student(*args):
    data = {}
    for k, v in zip(FIELDS, args):
        if "methods" in k or "groups" in k:
            data[k] = v.split(",") if v else []
        else:
            data[k] = v
    new_id = add_student(data)
    return (
        f"🎉 Created {new_id}",
        json.dumps(data, ensure_ascii=False, indent=2),
        gr.Dropdown.update(choices=list_student_ids(), value=new_id)
    )

def load_student_to_form(student_id: str):
    s = get_student(student_id)
    if not s: return [""] * len(FIELDS)
    return [", ".join(v) if isinstance(v, list) else v for v in s.values()]

def apply_update(student_id, *args):
    updates = {}
    for k, v in zip(FIELDS, args):
        if "methods" in k or "groups" in k:
            updates[k] = v.split(",") if v else []
        else:
            updates[k] = v
    msg = update_student(student_id, updates)
    return msg, json.dumps(get_student(student_id) or {}, ensure_ascii=False, indent=2)

# -----------------------------
# UI
# -----------------------------
THEME = gr.themes.Soft(primary_hue="indigo", secondary_hue="cyan")

CUSTOM_CSS = """
#header {padding: 24px 0 8px;}
#header h1 {margin:0; font-size: 2rem;}
.small {opacity:.85; font-size:.9rem}
.card {border:1px solid #2a2f4a; border-radius:12px; padding:12px;}
.gradio-container {background-color: #0b0e1a !important; color: #e0e0e0 !important;}
textarea, input, select {background-color: #14182b !important; border: 1px solid #2a2f4a !important; color: #e0e0e0 !important;}
button.primary {background: linear-gradient(90deg, #6C63FF, #00BCD4) !important; color: #fff !important;}
"""

with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
    gr.HTML("<div id='header'><h1>🎓 ThinkPal – Personalized Learning Assistant</h1></div>")

    with gr.Tab("💬 Chat"):
        with gr.Row():
            student_dd = gr.Dropdown(label="Select Student", choices=list_student_ids() or [])
            user_msg = gr.Textbox(label="Message", placeholder="Type roadmap, insights, or a question")
        ask_btn = gr.Button("Ask", variant="primary")
        chatbot_out = gr.Textbox(label="Response", lines=6, elem_classes=["card"])
        with gr.Row():
            roadmap_out = gr.Textbox(label="Roadmap", lines=10, elem_classes=["card"])
            insights_out = gr.Textbox(label="Insights", lines=10, elem_classes=["card"])
        ask_btn.click(fn=chat, inputs=[student_dd, user_msg], outputs=[roadmap_out, insights_out, chatbot_out])

    with gr.Tab("➕ Add Student"):
        fields = {k: gr.Textbox(label=k.replace("_", " ").title()) for k in FIELDS}
        create_btn = gr.Button("Create Profile", variant="primary")
        status_new = gr.Textbox(label="Status")
        preview_new = gr.Textbox(label="Saved Profile", lines=10)
        create_btn.click(fn=create_student, inputs=list(fields.values()), outputs=[status_new, preview_new, student_dd])

    with gr.Tab("✏️ Update Student"):
        target_id = gr.Dropdown(label="Student", choices=list_student_ids() or [])
        load_btn = gr.Button("Load Profile")
        upd_fields = {k: gr.Textbox(label=k.replace("_", " ").title()) for k in FIELDS}
        save_btn = gr.Button("Save Changes", variant="primary")
        status_upd = gr.Textbox(label="Status")
        preview_upd = gr.Textbox(label="Updated Profile", lines=10)
        load_btn.click(fn=load_student_to_form, inputs=[target_id], outputs=list(upd_fields.values()))
        save_btn.click(fn=apply_update, inputs=[target_id]+list(upd_fields.values()), outputs=[status_upd, preview_upd])

if __name__ == "__main__":
    demo.launch()