Download app.py from Yoans/ThinkPal: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/Yoans/ThinkPal/resolve/main/app.py
- Command line
-
hf download hf://spaces/Yoans/ThinkPal/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Yoans/ThinkPal/resolve/main/app.py
17.5 kB
| # 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() |