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() |