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