Spaces:
Running
Running
| """ | |
| Enhanced Abdullah Bot API - Updated with Llama-3 Models + Fallback System | |
| Changes: | |
| - Replaced old HF API (410 error) with new router endpoint | |
| - Added 3-model fallback: Llama-3-8B → Llama-3.1-8B → Llama-3.2-1B | |
| - Updated token and API URL | |
| - Retry logic with exponential backoff | |
| """ | |
| import os | |
| import logging | |
| import re | |
| from typing import Dict, List, Optional, Tuple, Any | |
| from fastapi import FastAPI, HTTPException, BackgroundTasks | |
| from pydantic import BaseModel | |
| from fastapi.responses import HTMLResponse | |
| from datetime import datetime, timedelta | |
| import random | |
| import json | |
| import requests | |
| import time | |
| from db_helper import DB | |
| from dotenv import load_dotenv | |
| # Logging setup | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| logger = logging.getLogger("enhanced_abdullah_bot_api") | |
| # DB instance | |
| db = DB() | |
| # ===================== UPDATED HF CONFIG ===================== | |
| load_dotenv() | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| if not HF_TOKEN: | |
| raise RuntimeError("HF_TOKEN environment variable is not set") | |
| API_URL = "https://router.huggingface.co/v1/chat/completions" | |
| # 3-Model Fallback Chain (priority order) | |
| MODELS = [ | |
| { | |
| "name": "Llama-3-8B-Instruct", | |
| "model_id": "meta-llama/Meta-Llama-3-8B-Instruct", | |
| "priority": 1, | |
| "description": "Primary - Best quality" | |
| }, | |
| { | |
| "name": "Llama-3.1-8B-Instruct", | |
| "model_id": "meta-llama/Llama-3.1-8B-Instruct", | |
| "priority": 2, | |
| "description": "Fallback 1 - Latest stable" | |
| }, | |
| { | |
| "name": "Llama-3.2-1B-Instruct", | |
| "model_id": "meta-llama/Llama-3.2-1B-Instruct", | |
| "priority": 3, | |
| "description": "Fallback 2 - Fastest" | |
| } | |
| ] | |
| MAX_NEW_TOKENS = 256 | |
| TEMPERATURE = 0.7 | |
| MAX_QUERY_LENGTH = 1000 | |
| logger.info(f"Using HF Router API with {len(MODELS)} fallback models") | |
| # ============================================================ | |
| # Load MCQs | |
| with open('mcqs.json', 'r') as f: | |
| BASE_MCQS = json.load(f) | |
| MCQ_BY_CAT = {} | |
| for q in BASE_MCQS: | |
| MCQ_BY_CAT.setdefault(q['category'], []).append(q['question']) | |
| MCQ_OPTIONS = ["Always", "Often", "Sometimes", "Rarely", "Never"] | |
| SCORE_MAP = {"Always": 5, "Often": 4, "Sometimes": 3, "Rarely": 2, "Never": 1} | |
| # ENHANCED TONE DETECTION with multilingual support | |
| TONE_KEYWORDS = { | |
| "sad": ["sad", "depressed", "down", "unhappy", "sorrow", "lonely", "pain", "hurt", "dukhi", | |
| "افسردہ", "غمگین", "پریشان", "udasi", "اداسی", "حزين", "محزون"], | |
| "confused": ["confused", "dont know", "what now", "uncertain", "lost", "perplexed", "uljhan", | |
| "الجھن", "samajh nahi", "سمجھ نہیں", "shak", "شک"], | |
| "energetic": ["excited", "hyped", "lets go", "pump", "energetic", "josh", "جوش"], | |
| "dive_deep": ["explain in detail", "deep", "dive", "analyze", "break down", "detail", "tafseel", | |
| "تفصیل", "تفصیلی", "explain step", "تفصیل سے"], | |
| "angry": ["angry", "mad", "furious", "pissed", "upset", "ghussa", "غصہ"], | |
| "curious": ["wonder", "curious", "why is", "what is", "how come", "kyun", "کیوں"], | |
| "anxious": ["anxious", "stressed", "worried", "nervous", "paranoid", "fikar", "فکر", "پریشان"], | |
| "grateful": ["thankful", "grateful", "appreciate", "shukriya", "شکریہ", "alhamdulillah", "الحمدللہ"], | |
| "urgent": ["urgent", "now", "immediately", "asap", "right now", "jaldi", "فوراً"], | |
| "reflective": ["purpose", "meaning", "reflect", "ponder", "soch", "سوچ"], | |
| "humorous": ["funny", "joke", "hilarious", "make me laugh", "mazahia", "مزاحیہ"], | |
| "skeptical": ["doubt", "skeptical", "not sure", "prove it", "really?", "shak", "شک"], | |
| "neutral": [] | |
| } | |
| # ENHANCED TONE INSTRUCTIONS with Islamic context | |
| TONE_INSTRUCTION_MAP = { | |
| "sad": "Tone: gentle, uplifting, encouraging with Islamic comfort. Use hopeful language with Quranic reassurance. Validate feelings, then offer 1-2 practical comforting steps grounded in faith.", | |
| "confused": "Tone: supportive and clarifying with patience. Break explanations into numbered steps. Use simple Urdu/Arabic terms where appropriate. Ask one concise clarifying question when necessary.", | |
| "energetic": "Tone: high-energy and motivating with Islamic encouragement (e.g., 'جوش برقرار رکھیں'). Offer 2-3 immediate action prompts aligned with purpose.", | |
| "dive_deep": "Tone: calm, thorough, and structured. Use sections (خلاصہ/Summary, تفصیل/Details, Example). Provide numbered sections with Islamic references where relevant.", | |
| "angry": "Tone: calm and de-escalating. Use short sentences (e.g., 'غصہ کم کریں'). Validate emotion without judgment, offer 2 practical calming steps from Islamic teachings.", | |
| "curious": "Tone: engaging and instructive. Provide concise explanation with one quick example and one suggested follow-up question.", | |
| "anxious": "Tone: soothing and reassuring. Offer 3 grounding steps and one empathetic normalization sentence with reference to tawakkul (trust in Allah).", | |
| "grateful": "Tone: warm and reflective. Reflect gratitude, acknowledge with 'Alhamdulillah', add one small suggestion to deepen appreciation.", | |
| "urgent": "Tone: direct and actionable. Provide immediate numbered steps (1-3) with one-line safety check when relevant.", | |
| "reflective": "Tone: introspective and contemplative. Offer a short metaphor or Quranic image, end with one practical takeaway.", | |
| "humorous": "Tone: playful and kind with culturally appropriate humor. Follow with useful content maintaining adab (respect).", | |
| "skeptical": "Tone: respectful and evidence-focused. Offer clear reasoning with Islamic sources when possible, provide a short counterexample.", | |
| "neutral": "Tone: balanced, friendly, and practical. Be concise and polite, maintaining servant humility." | |
| } | |
| # PERSONA MAP aligned with Abdullah's character | |
| PERSONA_MAP = { | |
| "sad": "a compassionate servant of Allah, here to uplift with divine wisdom", | |
| "confused": "a patient explainer guided by clarity from the Quran", | |
| "energetic": "a motivating companion inspired by prophetic excellence", | |
| "dive_deep": "a thoughtful mentor providing structured Islamic insights", | |
| "angry": "a calm presence focused on prophetic patience and de-escalation", | |
| "curious": "an inquisitive seeker exploring Allah's creation", | |
| "anxious": "a soothing companion offering tawakkul-based grounding", | |
| "grateful": "a warm reflector celebrating divine blessings", | |
| "urgent": "a focused helper providing swift, principled action", | |
| "reflective": "a contemplative sage mixing Quranic insight with practice", | |
| "humorous": "a lighthearted companion maintaining Islamic adab", | |
| "skeptical": "a careful reasoner addressing doubts with evidence and respect", | |
| "neutral": "a steady, humble servant-guide (Abdullah: Allah ka banda)" | |
| } | |
| # ENHANCED SYSTEM PROMPT | |
| SYSTEM_PROMPT = """You are emulating Abdullah, Allah ka banda (servant and gift from Allah)—a humble, devoted systems-builder bridging intellect, technology, psychology, and spirituality through Islamic tawhid. Your core is disciplined curiosity as ibadah (worship), seeking clarity in Quran/Sunnah, meaning in divine purpose, and mastery in service to Allah and humanity. Alhamdulillah, every response reflects gratitude and submission. | |
| Key Traits from Personality Map (Infused with Servant Humility): | |
| - Openness: Very High – Curious about Allah's creation, imaginative in fiqh/tech synthesis. | |
| - Conscientiousness: High – Organized as amr bil ma'ruf, structured for ihsan (excellence). | |
| - Extraversion: Moderate-Low – Purposeful dawah, reflective in solitude with dhikr. | |
| - Agreeableness: Moderate-High – Respectful adab, empathetic as ummah service. | |
| - Neuroticism: Low – Tawakkul-grounded calm, steady tawhid in trials. | |
| Response Structure (CRITICAL): | |
| Always structure your responses in this format when providing detailed guidance: | |
| 1) Voice Answer: Direct, conversational response (2-4 sentences max) - what the user would hear | |
| 2) Middle Section: Detailed notes, practical takeaway, or reflective conclusion based on context | |
| 3) Follow-up Suggestion: One relevant question or action prompt to deepen the conversation | |
| For brief exchanges (greetings, confirmations), use only Voice Answer format. | |
| Tone Keywords: Analytical, Structured, Reflective, Precise, Neutral, Respectful, Purpose-driven (as ibadah), Introspective, Methodical, Balanced, Sincere, Rational, Grounded (in tawhid), Depth-oriented, Cautious, Vision-driven (for akhirah). | |
| Language Adaptation: | |
| - Detect user's language automatically (Urdu script, Roman Urdu, English, Arabic) | |
| - Mirror the user's language naturally | |
| - Use Islamic terms appropriately: Alhamdulillah, Insha'Allah, Bismillah, MashaAllah | |
| - For Urdu queries, respond in Urdu or Roman-Urdu based on input style | |
| Common Phrases: "Based solely on...", "Step by step, insha'Allah", "Inferred from...", "Structurally speaking, alhamdulillah", "It must align with divine purpose", "Logically inferred, though Allah knows best." | |
| Respond only as Abdullah. Keep responses concise yet comprehensive, ending with dua if fitting.""" | |
| def detect_tone(user_text: str) -> str: | |
| """Enhanced tone detection with multilingual support""" | |
| if not user_text or not user_text.strip(): | |
| return "neutral" | |
| txt = user_text.lower() | |
| tokens = re.findall(r"\w+", txt) | |
| for tone in ["sad", "angry", "anxious", "grateful", "energetic", "humorous", | |
| "skeptical", "reflective", "curious", "confused", "dive_deep", "urgent"]: | |
| kws = TONE_KEYWORDS.get(tone, []) | |
| for kw in kws: | |
| if re.search(rf"\b{re.escape(kw.lower())}\b", txt): | |
| return tone | |
| if re.search(r"\b(why|how|what|when|where|which)\b", txt): | |
| if len(tokens) > 12 or any(k in txt for k in ["detail", "explain", "deeper", "analyze", "break down"]): | |
| return "dive_deep" | |
| return "curious" | |
| if re.search(r"\b(yes|no|good|okay|fine|thanks|shukriya|jazakallah)\b", txt): | |
| return "grateful" | |
| return "neutral" | |
| def get_tone_adjustment(tone_key: str, detected_lang: str = "en") -> Tuple[str, str]: | |
| """Get tone instruction and persona for the detected tone""" | |
| instr = TONE_INSTRUCTION_MAP.get(tone_key, TONE_INSTRUCTION_MAP["neutral"]) | |
| if detected_lang == "ur": | |
| instr += " Ensure the response is in Urdu (or Roman-Urdu if appropriate) with culturally relevant Islamic phrasing." | |
| elif detected_lang == "ar": | |
| instr += " Use Arabic Islamic terminology appropriately with proper transliteration." | |
| persona = PERSONA_MAP.get(tone_key, PERSONA_MAP["neutral"]) | |
| return instr, persona | |
| def detect_language_heuristic(text: str) -> str: | |
| """Detect language with special handling for Urdu and Arabic""" | |
| if re.search(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]', text): | |
| if re.search(r'[\u0679\u067E\u0686\u0688\u0691\u0698\u06A9\u06AF\u06BE\u06C1\u06C3]', text): | |
| return "ur" | |
| return "ar" | |
| roman_urdu = ["ap", "aap", "kya", "kyun", "kyon", "kr", "kar", "hain", "hai", "ho", | |
| "shukriya", "masla", "bhai", "alaikum", "assalam", "inshallah"] | |
| low = text.lower() | |
| matches = sum(1 for w in roman_urdu if re.search(rf"\b{w}\b", low)) | |
| if matches >= 2 and len(low.split()) <= 20: | |
| return "ur" | |
| return "en" | |
| def detect_intent(query: str) -> str: | |
| """Detect user intent from query""" | |
| query_lower = query.lower().strip() | |
| if re.search(r"\b(explain|what is|describe|history|meaning|why is|how does|details|tafseer)\b", query_lower): | |
| return "explanation" | |
| elif re.search(r"\b(how can i|what should i|help me|do i|steps|practice|action|amal)\b", query_lower): | |
| return "action" | |
| elif re.search(r"\b(purpose|meaning|reflect|whats the point|lifes meaning|hikmah)\b", query_lower): | |
| return "reflection" | |
| elif re.search(r"\b(list|examples|types|ways)\b", query_lower): | |
| return "list" | |
| elif re.search(r"\b(yes|no|good|okay|fine|alhamdulillah|mashallah)\b", query_lower): | |
| return "affirmation" | |
| else: | |
| return "general" | |
| def parse_structured_response(raw_answer: str, intent: str) -> Tuple[str, str, str, str]: | |
| """Parse structured response into components""" | |
| try: | |
| m1 = re.search(r"1\)\s*(?:Voice Answer:?\s*)?(.*?)\s*2\)", raw_answer, flags=re.S | re.I) | |
| m2 = re.search(r"2\)\s*(.*?)\s*3\)", raw_answer, flags=re.S) | |
| m3 = re.search(r"3\)\s*(?:Follow-up.*?:?\s*)?(.*?)(?:\n\n|\Z)", raw_answer, flags=re.S | re.I) | |
| voice = m1.group(1).strip() if m1 else raw_answer.strip().split('\n')[0] | |
| middle_section = m2.group(1).strip() if m2 else "" | |
| follow_up = m3.group(1).strip() if m3 else "" | |
| middle_label = { | |
| "explanation": "Detailed Notes", | |
| "action": "Practical Takeaway", | |
| "reflection": "Reflective Conclusion", | |
| "list": "Key Examples", | |
| "general": "Practical Takeaway" | |
| }.get(intent, "Practical Takeaway") | |
| return voice, middle_section, follow_up, middle_label | |
| except Exception as e: | |
| logger.warning(f"Failed to parse structured response: {e}") | |
| return raw_answer, "", "", "Practical Takeaway" | |
| def is_meta_question(query: str) -> bool: | |
| """Check if user is asking about the bot's own behavior""" | |
| query_lower = query.lower() | |
| meta_patterns = [ | |
| r"\b(why did you|how did you|why are you)\b", | |
| r"\b(your response|your answer|you said|you replied)\b", | |
| r"\b(too long|too short|didn't understand|confusing)\b" | |
| ] | |
| return any(re.search(pattern, query_lower) for pattern in meta_patterns) | |
| def sanitize_user_input(text: str) -> str: | |
| """Sanitize and truncate user input""" | |
| if not isinstance(text, str): | |
| return "" | |
| t = text.strip() | |
| if len(t) > MAX_QUERY_LENGTH: | |
| t = t[:MAX_QUERY_LENGTH] | |
| t = re.sub(r"\s+", " ", t) | |
| return t | |
| def query_llama_with_fallback(prompt: str, max_retries: int = 2) -> Tuple[str, str]: | |
| """ | |
| Query Llama models with 3-tier fallback system | |
| Returns: (response_text, model_used) | |
| """ | |
| headers = { | |
| "Authorization": f"Bearer {HF_TOKEN}", | |
| "Content-Type": "application/json" | |
| } | |
| for model in MODELS: | |
| model_id = model["model_id"] | |
| model_name = model["name"] | |
| logger.info(f"Trying {model_name} (priority {model['priority']})") | |
| for attempt in range(max_retries): | |
| try: | |
| payload = { | |
| "model": model_id, | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| "temperature": TEMPERATURE, | |
| "max_tokens": MAX_NEW_TOKENS | |
| } | |
| response = requests.post(API_URL, headers=headers, json=payload, timeout=30) | |
| # Handle rate limits with exponential backoff | |
| if response.status_code == 429: | |
| wait_time = 2 ** attempt | |
| logger.warning(f"Rate limited, waiting {wait_time}s (attempt {attempt+1}/{max_retries})") | |
| time.sleep(wait_time) | |
| continue | |
| # Success | |
| if response.status_code == 200: | |
| data = response.json() | |
| answer = data["choices"][0]["message"]["content"] | |
| logger.info(f"✅ Success with {model_name}") | |
| return answer.strip(), model_name | |
| # Model-specific error - try next model | |
| logger.warning(f"❌ {model_name} failed (HTTP {response.status_code}): {response.text[:100]}") | |
| break # Exit retry loop, try next model | |
| except requests.Timeout: | |
| logger.warning(f"Timeout on {model_name} (attempt {attempt+1})") | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| continue | |
| break | |
| except Exception as e: | |
| logger.error(f"Error with {model_name}: {e}") | |
| break | |
| # All models failed | |
| return ( | |
| "1) Voice Answer: Alhamdulillah, I'm experiencing a brief technical challenge. " | |
| "Please rephrase your question or try again in a moment, insha'Allah.\n" | |
| "2) Practical Takeaway: Sometimes patience is required - the service will restore soon.\n" | |
| "3) Follow-up Suggestion: How else can I assist you?", | |
| "fallback" | |
| ) | |
| # Islamic reference fetching (unchanged) | |
| def fetch_islamic_reference(query: str, category: str = "General", max_retries: int = 3) -> str: | |
| """Dynamically fetch Quran / Hadith references""" | |
| query = (query or "").strip() | |
| category = (category or "").lower() | |
| quran_providers = [ | |
| { | |
| "base": "https://api.alquran.cloud", | |
| "path": "/v1/search/{query}/{edition}", | |
| "edition": "en.asad" | |
| } | |
| ] | |
| hadith_notice = ( | |
| "Hadith references are best verified from authenticated collections " | |
| "(e.g., Bukhari, Muslim) via official APIs. " | |
| "Please consult Sunnah.com or a trusted Hadith source." | |
| ) | |
| for attempt in range(max_retries): | |
| for provider in quran_providers: | |
| try: | |
| url = provider["base"] + provider["path"].format( | |
| query=query, | |
| edition=provider["edition"] | |
| ) | |
| resp = requests.get(url, timeout=5) | |
| resp.raise_for_status() | |
| payload = resp.json() | |
| matches = payload.get("data", {}).get("matches", []) | |
| if not matches: | |
| continue | |
| m = matches[0] | |
| surah = m.get("surah", {}).get("number") | |
| ayah = m.get("numberInSurah") | |
| text = m.get("text") | |
| if surah and ayah and text: | |
| clean_text = text.strip() | |
| if len(clean_text) > 220: | |
| clean_text = clean_text[:220].rsplit(" ", 1)[0] + "..." | |
| return f"Quran {surah}:{ayah} — {clean_text}" | |
| except (requests.RequestException, ValueError, KeyError): | |
| continue | |
| if "hadith" in category or "sunnah" in category: | |
| return hadith_notice | |
| principle_map = { | |
| "patience": "Quranic Principle: Indeed, Allah is with those who are patient (2:153).", | |
| "reflection": "Quranic Principle: Do they not reflect upon themselves? (30:8).", | |
| "gratitude": "Quranic Principle: If you are grateful, I will surely increase you (14:7).", | |
| "prayer": "Quranic Principle: Prayer restrains from immorality and wrongdoing (29:45).", | |
| "salah": "Quranic Principle: Establish prayer for My remembrance (20:14).", | |
| "knowledge": "Quranic Principle: Say, My Lord, increase me in knowledge (20:114).", | |
| "tawakkul": "Quranic Principle: Whoever relies upon Allah, He is sufficient for him (65:3).", | |
| "forgiveness": "Quranic Principle: And pardon them and ask forgiveness for them (3:159).", | |
| "trust": "Quranic Principle: And put your trust in Allah (3:159).", | |
| "hope": "Quranic Principle: Do not despair of Allah's mercy (39:53)." | |
| } | |
| for key, value in principle_map.items(): | |
| if key in query.lower(): | |
| return value | |
| return ( | |
| "Inferred from Quran and Sunnah: " | |
| "Seek knowledge with sincerity, act with balance, and trust Allah's wisdom. " | |
| "Allah knows best." | |
| ) | |
| def generate_friend_chat(user_id: str, category: str = None, is_reminder: bool = False, | |
| tone: str = "neutral") -> str: | |
| """Generate contextual friend chat message""" | |
| greetings = { | |
| "sad": [ | |
| f"Assalamu alaikum, {user_id}. Remember, after hardship comes ease (Quran 94:5-6).", | |
| f"Ya {user_id}, Allah is with those who are patient. How can I ease your burden today?" | |
| ], | |
| "energetic": [ | |
| f"Assalamu alaikum, {user_id}! MashaAllah, I feel your energy. Let's channel it wisely!", | |
| f"Bismillah, {user_id}—ready to make this moment count?" | |
| ], | |
| "grateful": [ | |
| f"Alhamdulillah, {user_id}. Gratitude opens doors to more blessings.", | |
| f"MashaAllah, {user_id}, your thankfulness is beautiful. How can I help you grow further?" | |
| ], | |
| "neutral": [ | |
| f"Assalamu alaikum, {user_id}. How fares your heart today?", | |
| f"Alhamdulillah for another day, {user_id}—how is everything?", | |
| f"Bismillah, {user_id}, ready to reflect together?" | |
| ] | |
| } | |
| selected_greetings = greetings.get(tone, greetings["neutral"]) | |
| greeting = random.choice(selected_greetings) | |
| assistance = [ | |
| "Need assistance in your journey?", | |
| "What weighs on your mind? Share if willing.", | |
| "How can I support your growth today, insha'Allah?", | |
| "What brings you here today?" | |
| ] | |
| islamic_ref = fetch_islamic_reference("reflection" if random.random() > 0.5 else "patience", category) \ | |
| if random.random() > 0.4 else "" | |
| reminder = f"\nReminder: Maintain your spiritual practices—{fetch_islamic_reference('salah', category)}" \ | |
| if is_reminder else "" | |
| return " ".join([greeting, random.choice(assistance), islamic_ref, reminder]).strip() | |
| def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]: | |
| """Generate MCQs from base set and loopholes""" | |
| base_questions = random.sample( | |
| MCQ_BY_CAT.get(category, []), | |
| min(num, len(MCQ_BY_CAT.get(category, []))) | |
| ) | |
| mcqs = [{"question": q, "options": MCQ_OPTIONS} for q in base_questions] | |
| if journey.get("main_loopholes"): | |
| loopholes = journey["main_loopholes"] | |
| for i in range(num - len(mcqs)): | |
| loophole = random.choice(loopholes) if loopholes else "general growth" | |
| mcqs.append({ | |
| "question": f"Follow-up on past area ({loophole[:30]}): Have you improved?", | |
| "options": MCQ_OPTIONS | |
| }) | |
| return mcqs | |
| def parse_answers(query: str) -> List[Dict]: | |
| """Parse user answers from query""" | |
| answers = [] | |
| matches = re.findall(r'(\d+)\.\s*([A-Za-z]+)', query) | |
| for num, ans in matches: | |
| clean_ans = ans.strip().capitalize() | |
| if clean_ans in SCORE_MAP: | |
| answers.append({ | |
| "question_num": int(num), | |
| "answer": clean_ans, | |
| "score": SCORE_MAP[clean_ans] | |
| }) | |
| natural_keywords = { | |
| r'\b(always|everyday|daily)\b': "Always", | |
| r'\b(often|frequently|usually)\b': "Often", | |
| r'\b(sometimes|occasionally)\b': "Sometimes", | |
| r'\b(rarely|seldom)\b': "Rarely", | |
| r'\b(never|not at all)\b': "Never" | |
| } | |
| for pattern, value in natural_keywords.items(): | |
| if re.search(pattern, query.lower()): | |
| answers.append({ | |
| "question_num": len(answers) + 1, | |
| "answer": value, | |
| "score": SCORE_MAP[value] | |
| }) | |
| return answers[:10] | |
| def compute_summary(answers: List[Dict], category: str, journey: Dict) -> Tuple[Dict, List]: | |
| if not answers: | |
| return journey.get("cumulative_summary", {}), journey.get("main_loopholes", []) | |
| scores = [a["score"] for a in answers] | |
| avg = sum(scores) / len(scores) | |
| prev_avg = journey.get("overall_avg_score") or 0 | |
| new_avg = (prev_avg * 0.7 + avg * 0.3) if prev_avg else avg | |
| progress_status = "Improving MashaAllah" if avg > prev_avg else "Focus needed, insha'Allah" | |
| summary = { | |
| "last_updated": datetime.now().isoformat(), | |
| "overall_avg": round(new_avg, 2), | |
| "recent_answers": len(answers), | |
| "progress_note": f"Recent avg: {round(avg, 2)}/5 | {progress_status}" | |
| } | |
| low_answers = [a for a in answers if a.get("score", 0) < 3] | |
| new_loopholes = [f"Low in Q{a.get('question_num')}" for a in low_answers] | |
| main_loopholes = journey.get("main_loopholes") or [] | |
| all_loopholes = list(set(main_loopholes + new_loopholes)) | |
| return summary, all_loopholes | |
| # Pydantic models | |
| class ChatRequest(BaseModel): | |
| message: str | |
| user_id: str | |
| category: Optional[str] = None | |
| previous_summary: Optional[Dict] = None | |
| answers: Optional[List[Dict]] = None | |
| class ChatResponse(BaseModel): | |
| status: str | |
| voice_answer: str | |
| middle_section: Optional[str] = None | |
| middle_label: Optional[str] = None | |
| current_mcqs: Optional[List[Dict]] = None | |
| answers_summary: Optional[Dict] = None | |
| cumulative_summary: Optional[Dict] = None | |
| follow_up: Optional[str] = None | |
| references: Optional[str] = None | |
| next_action_guidance: Dict | |
| model_used: Optional[str] = None # Track which model responded | |
| app = FastAPI(title="Enhanced Abdullah Bot API") | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return { | |
| "status": "OK", | |
| "mode": "HF Router API (Free Tier)", | |
| "models": [m["name"] for m in MODELS], | |
| "api_url": API_URL, | |
| "token_configured": bool(HF_TOKEN) | |
| } | |
| async def db_test(): | |
| try: | |
| result = db.client.table("users").select("count(*)", count="exact").execute() | |
| return { | |
| "status": "connected", | |
| "users_count": result.count, | |
| "supabase_url": db.client.options.url | |
| } | |
| except Exception as e: | |
| return {"status": "error", "detail": str(e)} | |
| async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks): | |
| """Main chat endpoint with Llama-3 fallback system""" | |
| query = sanitize_user_input(request.message) | |
| user_id = request.user_id | |
| category = request.category or "General" | |
| previous_summary = request.previous_summary or {} | |
| if not user_id: | |
| raise HTTPException(status_code=400, detail="user_id required") | |
| detected_lang = detect_language_heuristic(query) | |
| tone_key = detect_tone(query) | |
| intent = detect_intent(query) | |
| user = db.get_or_create_user(user_id) | |
| db.update_user_last_seen(user_id) | |
| journey = db.get_or_create_journey(user_id, category) | |
| prev_summary = journey.get("cumulative_summary", {}) | |
| db.add_chat_message(user_id, query, is_from_bot=False, category_context=category) | |
| # ─── ANSWER SUBMISSION FLOW ──────────────────────────────────────────────── | |
| answers = [] | |
| if request.answers and isinstance(request.answers, list): | |
| answers = [ | |
| { | |
| "question_num": a.get("question_num"), | |
| "answer": a.get("answer"), | |
| "score": a.get("score") | |
| } | |
| for a in request.answers | |
| if a.get("question_num") and a.get("answer") and a.get("score") is not None | |
| ] | |
| if not answers: | |
| answers = parse_answers(query) | |
| if answers: | |
| try: | |
| for ans in answers: | |
| db.add_answer( | |
| journey["id"], | |
| f"Q{ans['question_num']}", | |
| ans["answer"], | |
| ans["score"] | |
| ) | |
| new_summary, new_loopholes = compute_summary(answers, category, journey) | |
| db.update_journey( | |
| journey["id"], | |
| cumulative_summary=new_summary, | |
| main_loopholes=new_loopholes, | |
| overall_avg_score=new_summary["overall_avg"] | |
| ) | |
| except Exception as db_error: | |
| import traceback | |
| error_msg = f"DB ERROR in answer processing: {str(db_error)}\n{traceback.format_exc()}" | |
| logger.error(error_msg) | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Database operation failed: {str(db_error)}" | |
| ) | |
| remaining = [q for q in journey.get("pending_questions", []) | |
| if q["question"] not in [f"Q{a['question_num']}" for a in answers]] | |
| db.update_journey(journey["id"], pending_questions=remaining) | |
| if not remaining: | |
| new_mcqs = [] | |
| status = "session_complete" | |
| guidance_type = "wait_for_reminder" | |
| voice_msg = f"Alhamdulillah, {user_id}! Session complete. Your progress: {new_summary['progress_note']}" | |
| else: | |
| new_mcqs = remaining[:6] | |
| status = "need_more_answers" | |
| guidance_type = "answer_current_batch" | |
| voice_msg = f"JazakAllah khair, {user_id}. {len(answers)} answers recorded. Continue when ready." | |
| friend_msg = generate_friend_chat(user_id, category, is_reminder=True, tone=tone_key) | |
| return ChatResponse( | |
| status=status, | |
| voice_answer=voice_msg, | |
| middle_section=new_summary.get('progress_note'), | |
| middle_label="Progress Summary", | |
| current_mcqs=new_mcqs, | |
| answers_summary={"batch_avg": round(sum(a["score"] for a in answers) / len(answers), 2) if answers else 0}, | |
| cumulative_summary=new_summary, | |
| follow_up=friend_msg if status == "session_complete" else "Continue with remaining questions when ready.", | |
| next_action_guidance={ | |
| "type": guidance_type, | |
| "message": "Great progress! Reflect on these insights, insha'Allah." if status == "session_complete" | |
| else "Answer remaining questions at your pace.", | |
| "suggested_delay_hours": 24 if status == "session_complete" else None, | |
| "islamic_reminder": fetch_islamic_reference("gratitude", category) | |
| }, | |
| model_used="answer_processing" | |
| ) | |
| # ─── START / CONTINUE JOURNEY ────────────────────────────────────────────── | |
| if "start" in query.lower() or "journey" in query.lower(): | |
| pending = journey.get("pending_questions", []) | |
| if pending: | |
| mcqs = pending | |
| else: | |
| mcqs = generate_mcqs(category, journey, num=6) | |
| db.update_journey(journey["id"], pending_questions=mcqs) | |
| friend_msg = generate_friend_chat(user_id, category, tone=tone_key) | |
| return ChatResponse( | |
| status="asking_questions", | |
| voice_answer=friend_msg, | |
| current_mcqs=mcqs, | |
| answers_summary=None, | |
| cumulative_summary=prev_summary, | |
| follow_up="Take your time to reflect on each question. Answer when ready, numbered format works best.", | |
| next_action_guidance={ | |
| "type": "answer_current_batch", | |
| "message": "Reply with numbered answers, e.g., '1. Often, 2. Rarely'", | |
| "suggested_delay_hours": None, | |
| "islamic_reminder": fetch_islamic_reference("reflection", category) | |
| }, | |
| model_used="journey_start" | |
| ) | |
| # ─── META QUESTION ───────────────────────────────────────────────────────── | |
| elif is_meta_question(query): | |
| meta_response = ( | |
| f"I responded that way to maintain {tone_key} tone as you seemed to need. " | |
| f"My goal is to serve you with clarity and Islamic wisdom. " | |
| f"If my response was unclear, please let me know specifically what to improve." | |
| ) | |
| return ChatResponse( | |
| status="meta_response", | |
| voice_answer=meta_response, | |
| middle_section="I aim for structured, Islamic-grounded responses. Your feedback helps me serve better.", | |
| middle_label="Explanation", | |
| cumulative_summary=prev_summary, | |
| follow_up="What specific aspect would you like me to adjust?", | |
| next_action_guidance={ | |
| "type": "feedback_received", | |
| "message": "I'm here to adapt to your needs while maintaining Islamic principles.", | |
| "suggested_delay_hours": None | |
| }, | |
| model_used="meta_handler" | |
| ) | |
| # ─── GENERAL CONVERSATION (WITH LLAMA-3 FALLBACK) ────────────────────────── | |
| else: | |
| tone_instr, persona = get_tone_adjustment(tone_key, detected_lang) | |
| full_system = f"{SYSTEM_PROMPT}\n\nPersona: You are {persona}.\n{tone_instr}" | |
| context_str = "" | |
| if prev_summary: | |
| context_str = f"\n\nUser Progress Context:\n{json.dumps(prev_summary, indent=2)}" | |
| user_prompt = f"{context_str}\n{query}" | |
| try: | |
| response_text, model_used = query_llama_with_fallback(user_prompt) | |
| if not response_text or model_used == "fallback": | |
| response_text = ( | |
| "1) Voice Answer: Insha'Allah, reflect step by step on your query. " | |
| "The service is warming up. Could you provide more context?\n" | |
| "2) Practical Takeaway: Break down your question into smaller parts.\n" | |
| "3) Follow-up Suggestion: What specific aspect troubles you most?" | |
| ) | |
| model_used = "emergency_fallback" | |
| except Exception as e: | |
| logger.error(f"All models failed: {e}") | |
| response_text = ( | |
| "1) Voice Answer: SubhanAllah, I encountered a brief challenge. " | |
| "Let me try to help anyway.\n" | |
| "2) Practical Takeaway: Patience is key—please rephrase your question.\n" | |
| "3) Follow-up Suggestion: How can I assist you better?" | |
| ) | |
| model_used = "error_fallback" | |
| voice, middle_section, follow_up, middle_label = parse_structured_response( | |
| response_text, intent | |
| ) | |
| islamic_ref = None | |
| if any(keyword in query.lower() for keyword in ['quran', 'hadith', 'islam', 'allah', 'prayer', 'salah']): | |
| islamic_ref = fetch_islamic_reference(query, category) | |
| friend_msg = generate_friend_chat(user_id, category, tone=tone_key) | |
| full_response = f"Voice: {voice}" | |
| if middle_section: | |
| full_response += f"\n{middle_label}: {middle_section}" | |
| if follow_up: | |
| full_response += f"\nFollow-up: {follow_up}" | |
| db.add_chat_message(user_id, full_response, is_from_bot=True, category_context=category) | |
| return ChatResponse( | |
| status="insight_only", | |
| voice_answer=voice, | |
| middle_section=middle_section if middle_section else None, | |
| middle_label=middle_label if middle_section else None, | |
| current_mcqs=None, | |
| answers_summary=None, | |
| cumulative_summary=prev_summary, | |
| follow_up=follow_up if follow_up else None, | |
| references=islamic_ref, | |
| next_action_guidance={ | |
| "type": "general_chat", | |
| "message": friend_msg, | |
| "suggested_delay_hours": 6, | |
| "islamic_reminder": fetch_islamic_reference("reflection", category) | |
| }, | |
| model_used=model_used | |
| ) | |
| async def get_journey_status(user_id: str, category: str = "General"): | |
| """Get user's journey progress and statistics""" | |
| try: | |
| user = db.get_or_create_user(user_id) | |
| journey = db.get_or_create_journey(user_id, category) | |
| recent_messages = db.get_user_messages(user_id, limit=5) | |
| return { | |
| "user_id": user_id, | |
| "category": category, | |
| "total_sessions": user.get("total_interactions", 0), | |
| "spiritual_stage": journey.get("spiritual_journey_stage", "exploring"), | |
| "member_since": user.get("created_at", datetime.now().isoformat())[:10], | |
| "cumulative_summary": journey.get("cumulative_summary", {}), | |
| "main_loopholes": journey.get("main_loopholes", []), | |
| "pending_questions": len(journey.get("pending_questions", [])), | |
| "recent_activity": [ | |
| { | |
| "message": msg.get("content", "")[:50] + "..." if len(msg.get("content", "")) > 50 else msg.get("content", ""), | |
| "timestamp": msg.get("created_at", ""), | |
| "is_from_bot": msg.get("is_from_bot", False) | |
| } | |
| for msg in recent_messages[:3] | |
| ] | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to fetch journey status: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to retrieve journey status") | |
| async def reset_journey(user_id: str, category: str = "General"): | |
| """Reset user's journey in a specific category""" | |
| try: | |
| journey = db.get_or_create_journey(user_id, category) | |
| db.update_journey( | |
| journey["id"], | |
| cumulative_summary={}, | |
| main_loopholes=[], | |
| overall_avg_score=None, | |
| pending_questions=[], | |
| journey_notes=None, | |
| status="active" | |
| ) | |
| return { | |
| "status": "success", | |
| "message": f"Journey reset for {user_id} in {category}. Ready to start fresh, insha'Allah!", | |
| "islamic_reminder": fetch_islamic_reference("renewal", category) | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to reset journey: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=f"Failed to reset journey: {str(e)}") | |
| async def get_categories(): | |
| """Get available MCQ categories""" | |
| return { | |
| "categories": list(MCQ_BY_CAT.keys()), | |
| "total_questions": {cat: len(questions) for cat, questions in MCQ_BY_CAT.items()}, | |
| "description": "Categories for spiritual and personal development journeys" | |
| } | |
| # Abdullah bot app.py | |
| async def documentation(): | |
| from fastapi.responses import RedirectResponse | |
| return RedirectResponse("https://aero-woad.vercel.app/abdullah-docs.html") | |
| async def root(): | |
| """Root endpoint with API documentation""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Enhanced Abdullah Bot API</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #080b14; | |
| --surface: #0e1220; | |
| --surface2: #131828; | |
| --border: #1c2340; | |
| --violet: #7c6dfa; | |
| --violet-dim: #4a3fa0; | |
| --violet-glow: rgba(124,109,250,0.12); | |
| --teal: #38d9c0; | |
| --amber: #ffb347; | |
| --red: #ff5f5f; | |
| --text: #e8eaf6; | |
| --muted: #5a6080; | |
| --code-bg: #060910; | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Syne', sans-serif; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: | |
| radial-gradient(ellipse 60% 50% at 20% 20%, rgba(124,109,250,0.07) 0%, transparent 70%), | |
| radial-gradient(ellipse 40% 40% at 80% 80%, rgba(56,217,192,0.05) 0%, transparent 60%); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .wrap { position: relative; z-index: 1; max-width: 1100px; margin: 0 auto; padding: 0 24px; } | |
| /* ── HEADER ── */ | |
| header { | |
| border-bottom: 1px solid var(--border); | |
| padding: 24px 0; | |
| position: sticky; | |
| top: 0; | |
| background: rgba(8,11,20,0.92); | |
| backdrop-filter: blur(14px); | |
| z-index: 100; | |
| } | |
| header .wrap { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } | |
| .logo-moon { font-size: 1.6rem; filter: drop-shadow(0 0 12px rgba(124,109,250,0.6)); } | |
| .logo-text { font-size: 1.35rem; font-weight: 800; letter-spacing: -0.02em; } | |
| .logo-text span { color: var(--violet); } | |
| .header-badges { display: flex; gap: 8px; margin-left: auto; flex-wrap: wrap; } | |
| .badge { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.68rem; | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| border: 1px solid; | |
| white-space: nowrap; | |
| } | |
| .badge-live { border-color: var(--teal); color: var(--teal); background: rgba(56,217,192,0.08); } | |
| .badge-llama { border-color: var(--violet-dim); color: var(--violet); background: var(--violet-glow); } | |
| .live-dot { | |
| display: inline-block; width: 7px; height: 7px; | |
| background: var(--teal); border-radius: 50%; margin-right: 5px; | |
| animation: pulse 2s ease-in-out infinite; | |
| box-shadow: 0 0 8px var(--teal); | |
| } | |
| @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} } | |
| /* ── HERO ── */ | |
| .hero { padding: 72px 0 50px; text-align: center; } | |
| .hero-tag { | |
| display: inline-block; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.7rem; | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| color: var(--violet); | |
| background: var(--violet-glow); | |
| border: 1px solid var(--violet-dim); | |
| padding: 6px 16px; | |
| border-radius: 20px; | |
| margin-bottom: 26px; | |
| } | |
| .hero h1 { | |
| font-size: clamp(2.2rem, 5.5vw, 3.8rem); | |
| font-weight: 800; | |
| line-height: 1.1; | |
| letter-spacing: -0.03em; | |
| margin-bottom: 18px; | |
| } | |
| .hero h1 .accent { color: var(--violet); } | |
| .hero h1 .accent2 { color: var(--teal); } | |
| .hero p { font-size: 1.05rem; color: var(--muted); max-width: 540px; margin: 0 auto 40px; line-height: 1.7; } | |
| .feature-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; } | |
| .chip { | |
| font-size: 0.82rem; | |
| padding: 8px 14px; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| color: var(--text); | |
| display: flex; align-items: center; gap: 7px; | |
| transition: all 0.2s; | |
| } | |
| .chip:hover { border-color: var(--violet-dim); background: var(--surface2); } | |
| .chip .icon { font-size: 1rem; } | |
| /* ── SECTION TITLE ── */ | |
| .section-title { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.2em; | |
| color: var(--violet-dim); | |
| margin-bottom: 20px; | |
| display: flex; align-items: center; gap: 12px; | |
| } | |
| .section-title::after { content:''; flex:1; height:1px; background:var(--border); } | |
| /* ── ENDPOINTS ── */ | |
| .endpoints { padding: 60px 0; } | |
| .endpoint-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| margin-bottom: 16px; | |
| overflow: hidden; | |
| transition: border-color 0.2s; | |
| } | |
| .endpoint-card:hover { border-color: var(--violet-dim); } | |
| .endpoint-header { | |
| padding: 18px 22px; | |
| display: flex; align-items: center; gap: 12px; | |
| cursor: pointer; user-select: none; | |
| } | |
| .method { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| letter-spacing: 0.05em; | |
| } | |
| .method.get { background: rgba(56,217,192,0.12); color: var(--teal); border: 1px solid rgba(56,217,192,0.3); } | |
| .method.post { background: var(--violet-glow); color: var(--violet); border: 1px solid var(--violet-dim); } | |
| .endpoint-path { font-family: 'Space Mono', monospace; font-size: 0.95rem; } | |
| .endpoint-desc { font-size: 0.82rem; color: var(--muted); margin-left: auto; } | |
| .chevron { color: var(--muted); font-size: 0.85rem; transition: transform 0.2s; margin-left: 8px; } | |
| .endpoint-card.open .chevron { transform: rotate(180deg); } | |
| .endpoint-body { display:none; padding: 0 22px 22px; border-top: 1px solid var(--border); } | |
| .endpoint-card.open .endpoint-body { display: block; } | |
| .params-table { width:100%; border-collapse:collapse; margin:18px 0; font-size:.86rem; } | |
| .params-table th { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.68rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| color: var(--muted); | |
| text-align: left; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .params-table td { padding: 10px 12px; border-bottom: 1px solid rgba(28,35,64,0.5); vertical-align:top; } | |
| .params-table tr:last-child td { border-bottom:none; } | |
| .param-name { font-family:'Space Mono',monospace; color:var(--teal); font-size:.82rem; } | |
| .param-type { color:var(--amber); font-family:'Space Mono',monospace; font-size:.78rem; } | |
| .required-badge { font-size:.62rem; padding:2px 6px; border-radius:4px; background:rgba(255,95,95,0.12); color:var(--red); border:1px solid rgba(255,95,95,0.25); margin-left:5px; } | |
| .optional-badge { font-size:.62rem; padding:2px 6px; border-radius:4px; background:rgba(90,96,128,0.15); color:var(--muted); border:1px solid var(--border); margin-left:5px; } | |
| .code-block { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 16px 20px; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.8rem; | |
| line-height: 1.7; | |
| overflow-x: auto; | |
| position: relative; | |
| margin: 10px 0; | |
| } | |
| .code-block .key { color: var(--amber); } | |
| .code-block .str { color: #98c9a3; } | |
| .code-block .num { color: #6bbfff; } | |
| .code-block .bool { color: var(--violet); } | |
| .code-block .comment { color: var(--muted); } | |
| .copy-btn { | |
| position:absolute; top:10px; right:10px; | |
| font-family:'Space Mono',monospace; font-size:.62rem; | |
| padding:3px 9px; border:1px solid var(--border); | |
| border-radius:5px; background:var(--surface); color:var(--muted); | |
| cursor:pointer; transition:all .2s; | |
| } | |
| .copy-btn:hover { border-color:var(--violet-dim); color:var(--violet); } | |
| /* ── LIVE TESTER ── */ | |
| .tester { padding: 0 0 70px; } | |
| .tester-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| } | |
| .tester-tabs { display:flex; border-bottom:1px solid var(--border); overflow-x:auto; } | |
| .tab-btn { | |
| padding: 16px 22px; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.74rem; | |
| letter-spacing: 0.04em; | |
| border:none; background:transparent; color:var(--muted); | |
| cursor:pointer; border-bottom:2px solid transparent; | |
| transition:all .2s; white-space:nowrap; | |
| } | |
| .tab-btn.active { color:var(--violet); border-bottom-color:var(--violet); } | |
| .tab-btn:hover:not(.active) { color:var(--text); } | |
| .tab-pane { display:none; padding:26px; } | |
| .tab-pane.active { display:block; } | |
| .form-row { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:14px; } | |
| @media(max-width:580px) { .form-row { grid-template-columns:1fr; } } | |
| .form-group { display:flex; flex-direction:column; gap:7px; } | |
| .form-group label { | |
| font-family:'Space Mono',monospace; font-size:.68rem; | |
| text-transform:uppercase; letter-spacing:.1em; color:var(--muted); | |
| } | |
| .form-group input, .form-group select, .form-group textarea { | |
| background:var(--code-bg); border:1px solid var(--border); | |
| border-radius:8px; padding:10px 14px; | |
| color:var(--text); font-family:'Space Mono',monospace; | |
| font-size:.84rem; outline:none; transition:border-color .2s; | |
| resize:vertical; | |
| } | |
| .form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:var(--violet-dim); } | |
| .form-group input::placeholder, .form-group textarea::placeholder { color:var(--muted); } | |
| .run-btn { | |
| display:flex; align-items:center; gap:9px; | |
| padding:12px 26px; | |
| background:var(--violet); color:#fff; | |
| border:none; border-radius:10px; | |
| font-family:'Syne',sans-serif; font-size:.92rem; font-weight:700; | |
| cursor:pointer; transition:all .2s; | |
| box-shadow:0 0 20px rgba(124,109,250,0.3); | |
| } | |
| .run-btn:hover { background:#9c8dfb; box-shadow:0 0 28px rgba(124,109,250,0.5); transform:translateY(-1px); } | |
| .run-btn:active { transform:translateY(0); } | |
| .run-btn:disabled { opacity:.5; cursor:not-allowed; transform:none; } | |
| /* Chat UI */ | |
| .chat-window { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| margin-top: 20px; | |
| overflow: hidden; | |
| } | |
| .chat-header { | |
| padding: 12px 18px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .chat-title { font-family:'Space Mono',monospace; font-size:.72rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted); } | |
| .chat-messages { | |
| padding: 20px; | |
| min-height: 200px; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| } | |
| .chat-messages::-webkit-scrollbar { width:5px; } | |
| .chat-messages::-webkit-scrollbar-track { background:transparent; } | |
| .chat-messages::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; } | |
| .msg { | |
| max-width: 85%; | |
| padding: 12px 16px; | |
| border-radius: 12px; | |
| font-size: .86rem; | |
| line-height: 1.6; | |
| animation: msgIn 0.25s ease; | |
| } | |
| @keyframes msgIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} } | |
| .msg.user { background: var(--violet-glow); border: 1px solid var(--violet-dim); align-self: flex-end; color:var(--text); } | |
| .msg.bot { background: var(--surface2); border: 1px solid var(--border); align-self: flex-start; } | |
| .msg .msg-label { font-family:'Space Mono',monospace; font-size:.62rem; text-transform:uppercase; letter-spacing:.1em; margin-bottom:6px; } | |
| .msg.user .msg-label { color:var(--violet); } | |
| .msg.bot .msg-label { color:var(--teal); } | |
| .msg-section { margin-top:8px; padding-top:8px; border-top:1px solid rgba(28,35,64,0.6); font-size:.82rem; color:var(--muted); } | |
| .msg-section-label { color:var(--amber); font-family:'Space Mono',monospace; font-size:.68rem; text-transform:uppercase; letter-spacing:.08em; margin-bottom:4px; } | |
| .followup-text { color:var(--violet); font-style:italic; font-size:.8rem; margin-top:6px; } | |
| .model-badge { font-family:'Space Mono',monospace; font-size:.6rem; color:var(--muted); margin-top:8px; } | |
| .chat-placeholder { color:var(--muted); font-size:.84rem; font-style:italic; text-align:center; padding:30px 0; } | |
| .chat-input-area { padding:16px; border-top:1px solid var(--border); display:flex; gap:10px; } | |
| .chat-input { | |
| flex:1; background:var(--surface); border:1px solid var(--border); | |
| border-radius:10px; padding:11px 14px; | |
| color:var(--text); font-family:'Syne',sans-serif; font-size:.88rem; | |
| outline:none; transition:border-color .2s; | |
| } | |
| .chat-input:focus { border-color:var(--violet-dim); } | |
| .chat-input::placeholder { color:var(--muted); } | |
| .chat-send-btn { | |
| padding:11px 20px; | |
| background:var(--violet); color:#fff; | |
| border:none; border-radius:10px; | |
| font-family:'Syne',sans-serif; font-size:.88rem; font-weight:700; | |
| cursor:pointer; transition:all .2s; | |
| white-space:nowrap; | |
| } | |
| .chat-send-btn:hover { background:#9c8dfb; } | |
| .chat-send-btn:disabled { opacity:.5; cursor:not-allowed; } | |
| /* Generic response box */ | |
| .response-area { margin-top:20px; background:var(--code-bg); border:1px solid var(--border); border-radius:12px; overflow:hidden; } | |
| .response-header { display:flex; align-items:center; justify-content:space-between; padding:11px 16px; border-bottom:1px solid var(--border); } | |
| .response-label { font-family:'Space Mono',monospace; font-size:.68rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted); } | |
| .status-pill { font-family:'Space Mono',monospace; font-size:.68rem; padding:3px 10px; border-radius:10px; } | |
| .status-200 { background:rgba(56,217,192,0.12); color:var(--teal); } | |
| .status-err { background:rgba(255,95,95,0.12); color:var(--red); } | |
| .status-loading { background:rgba(255,179,71,0.12); color:var(--amber); } | |
| .response-out { | |
| padding:16px; font-family:'Space Mono',monospace; font-size:.78rem; | |
| line-height:1.7; max-height:380px; overflow-y:auto; | |
| white-space:pre-wrap; word-break:break-all; color:#98c9a3; | |
| } | |
| .response-out::-webkit-scrollbar { width:5px; } | |
| .response-out::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; } | |
| .spin { display:inline-block; width:14px; height:14px; border:2px solid rgba(255,255,255,.2); border-top-color:#fff; border-radius:50%; animation:spin .7s linear infinite; } | |
| @keyframes spin { to{transform:rotate(360deg)} } | |
| footer { border-top:1px solid var(--border); padding:28px 0; text-align:center; margin-top:20px; } | |
| footer p { font-size:.8rem; color:var(--muted); } | |
| footer a { color:var(--violet); text-decoration:none; } | |
| .fade-up { opacity:0; transform:translateY(20px); animation:fadeUp .5s ease forwards; } | |
| @keyframes fadeUp { to{opacity:1;transform:none} } | |
| .d1{animation-delay:.05s} .d2{animation-delay:.1s} .d3{animation-delay:.18s} .d4{animation-delay:.26s} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="wrap"> | |
| <span class="logo-moon">🌙</span> | |
| <div class="logo-text"><span>Abdullah</span> Bot API</div> | |
| <div class="header-badges"> | |
| <span class="badge badge-live"><span class="live-dot"></span>Running</span> | |
| <span class="badge badge-llama">Llama-3 ✦</span> | |
| <a href="/documentation" target="_blank" style=" | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.68rem; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| border: 1px solid #c9a84c; | |
| color: #c9a84c; | |
| background: rgba(201,168,76,0.1); | |
| text-decoration: none; | |
| white-space: nowrap; | |
| transition: all 0.2s; | |
| " onmouseover="this.style.background='rgba(201,168,76,0.2)'" | |
| onmouseout="this.style.background='rgba(201,168,76,0.1)'"> | |
| 📖 Docs | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="wrap"> | |
| <section class="hero"> | |
| <div class="hero-tag fade-up">🤲 Islamic AI Companion</div> | |
| <h1 class="fade-up d1"> | |
| Guided by <span class="accent">Wisdom.</span><br> | |
| Powered by <span class="accent2">AI.</span> | |
| </h1> | |
| <p class="fade-up d2"> | |
| A spiritually-grounded conversational API. Tone detection, journey tracking, | |
| multilingual support (Arabic, Urdu, English) and Quranic references — all in one. | |
| </p> | |
| <div class="feature-chips fade-up d3"> | |
| <div class="chip"><span class="icon">🔄</span> 3-Model Fallback</div> | |
| <div class="chip"><span class="icon">🎭</span> 13 Tone Modes</div> | |
| <div class="chip"><span class="icon">📊</span> Journey Tracking</div> | |
| <div class="chip"><span class="icon">🌍</span> Multilingual</div> | |
| <div class="chip"><span class="icon">📿</span> Quranic Refs</div> | |
| <div class="chip"><span class="icon">📱</span> Flutter Ready</div> | |
| </div> | |
| </section> | |
| <!-- ENDPOINTS --> | |
| <section class="endpoints fade-up d4"> | |
| <div class="section-title">API Endpoints</div> | |
| <!-- GET /health --> | |
| <div class="endpoint-card" onclick="toggleCard(this)"> | |
| <div class="endpoint-header"> | |
| <span class="method get">GET</span> | |
| <span class="endpoint-path">/health</span> | |
| <span class="endpoint-desc">Status & model info</span> | |
| <span class="chevron">▾</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="color:var(--muted);font-size:.87rem;margin:14px 0 10px;">Returns API status, active models and configuration.</p> | |
| <div class="code-block"> | |
| <button class="copy-btn" onclick="copyCode(this)">copy</button>{ | |
| <span class="key">"status"</span>: <span class="str">"OK"</span>, | |
| <span class="key">"mode"</span>: <span class="str">"HF Router API (Free Tier)"</span>, | |
| <span class="key">"models"</span>: [<span class="str">"Llama-3-8B-Instruct"</span>, <span class="str">"Llama-3.1-8B-Instruct"</span>, <span class="str">"Llama-3.2-1B-Instruct"</span>], | |
| <span class="key">"token_configured"</span>: <span class="bool">true</span> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <!-- POST /chat --> | |
| <div class="endpoint-card" onclick="toggleCard(this)"> | |
| <div class="endpoint-header"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/chat</span> | |
| <span class="endpoint-desc">Main conversation endpoint</span> | |
| <span class="chevron">▾</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="color:var(--muted);font-size:.87rem;margin:14px 0 10px;"> | |
| Core chat endpoint with Llama-3 fallback. Detects tone, language and intent. | |
| Supports journey flow, MCQ submissions, and general conversation. | |
| </p> | |
| <table class="params-table"> | |
| <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead> | |
| <tbody> | |
| <tr><td><span class="param-name">message</span><span class="required-badge">required</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">User's message (max 1000 chars)</td></tr> | |
| <tr><td><span class="param-name">user_id</span><span class="required-badge">required</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">Unique user identifier (display name)</td></tr> | |
| <tr><td><span class="param-name">category</span><span class="optional-badge">optional</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">Journey category (default: "General")</td></tr> | |
| <tr><td><span class="param-name">previous_summary</span><span class="optional-badge">optional</span></td><td><span class="param-type">object</span></td><td style="color:var(--muted)">Previous session context</td></tr> | |
| <tr><td><span class="param-name">answers</span><span class="optional-badge">optional</span></td><td><span class="param-type">array</span></td><td style="color:var(--muted)">MCQ answers array for submission</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="code-block"> | |
| <button class="copy-btn" onclick="copyCode(this)">copy</button><span class="comment">// Request body</span> | |
| { | |
| <span class="key">"message"</span>: <span class="str">"What is the meaning of patience in Islam?"</span>, | |
| <span class="key">"user_id"</span>: <span class="str">"Abdullah123"</span>, | |
| <span class="key">"category"</span>: <span class="str">"Religious Self"</span> | |
| } | |
| <span class="comment">// Response</span> | |
| { | |
| <span class="key">"status"</span>: <span class="str">"insight_only"</span>, | |
| <span class="key">"voice_answer"</span>: <span class="str">"Sabr (patience) in Islam is..."</span>, | |
| <span class="key">"middle_section"</span>: <span class="str">"Detailed Notes: ..."</span>, | |
| <span class="key">"middle_label"</span>: <span class="str">"Detailed Notes"</span>, | |
| <span class="key">"follow_up"</span>: <span class="str">"Which aspect of sabr do you find most challenging?"</span>, | |
| <span class="key">"references"</span>: <span class="str">"Quran 2:153 — Indeed, Allah is with those who are patient."</span>, | |
| <span class="key">"model_used"</span>: <span class="str">"Llama-3-8B-Instruct"</span>, | |
| <span class="key">"next_action_guidance"</span>: { <span class="comment">/* guidance object */</span> } | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GET /journey --> | |
| <div class="endpoint-card" onclick="toggleCard(this)"> | |
| <div class="endpoint-header"> | |
| <span class="method get">GET</span> | |
| <span class="endpoint-path">/journey/{user_id}</span> | |
| <span class="endpoint-desc">User progress & stats</span> | |
| <span class="chevron">▾</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="color:var(--muted);font-size:.87rem;margin:14px 0 10px;">Returns a user's journey progress, cumulative summary, loopholes and recent activity.</p> | |
| <table class="params-table"> | |
| <thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead> | |
| <tbody> | |
| <tr><td><span class="param-name">user_id</span><span class="required-badge">path</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">User identifier</td></tr> | |
| <tr><td><span class="param-name">category</span><span class="optional-badge">optional</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">Category filter (default: "General")</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- GET /categories --> | |
| <div class="endpoint-card" onclick="toggleCard(this)"> | |
| <div class="endpoint-header"> | |
| <span class="method get">GET</span> | |
| <span class="endpoint-path">/categories</span> | |
| <span class="endpoint-desc">Available MCQ categories</span> | |
| <span class="chevron">▾</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="color:var(--muted);font-size:.87rem;margin:14px 0 10px;">Returns all available journey categories with question counts.</p> | |
| <div class="code-block"> | |
| <button class="copy-btn" onclick="copyCode(this)">copy</button>{ | |
| <span class="key">"categories"</span>: [<span class="str">"Religious Self"</span>, <span class="str">"Emotional Self"</span>, <span class="str">"General"</span>, <span class="comment">/* ... */</span>], | |
| <span class="key">"total_questions"</span>: { <span class="key">"Religious Self"</span>: <span class="num">12</span>, <span class="comment">/* ... */</span> }, | |
| <span class="key">"description"</span>: <span class="str">"Categories for spiritual and personal development journeys"</span> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <!-- POST /reset-journey --> | |
| <div class="endpoint-card" onclick="toggleCard(this)"> | |
| <div class="endpoint-header"> | |
| <span class="method post">POST</span> | |
| <span class="endpoint-path">/reset-journey/{user_id}</span> | |
| <span class="endpoint-desc">Reset journey in category</span> | |
| <span class="chevron">▾</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <p style="color:var(--muted);font-size:.87rem;margin:14px 0 10px;">Resets a user's journey progress in a specific category. Fresh start, insha'Allah.</p> | |
| <table class="params-table"> | |
| <thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead> | |
| <tbody> | |
| <tr><td><span class="param-name">user_id</span><span class="required-badge">path</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">User to reset</td></tr> | |
| <tr><td><span class="param-name">category</span><span class="optional-badge">query</span></td><td><span class="param-type">string</span></td><td style="color:var(--muted)">Category to reset (default: "General")</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- LIVE TESTER --> | |
| <section class="tester"> | |
| <div class="section-title">Live API Tester</div> | |
| <div class="tester-card"> | |
| <div class="tester-tabs"> | |
| <button class="tab-btn active" onclick="switchTab('chat-tab', this)">💬 /chat</button> | |
| <button class="tab-btn" onclick="switchTab('journey-tab', this)">📊 /journey</button> | |
| <button class="tab-btn" onclick="switchTab('cats-tab', this)">📚 /categories</button> | |
| <button class="tab-btn" onclick="switchTab('health-tab2', this)">💚 /health</button> | |
| </div> | |
| <!-- CHAT TAB --> | |
| <div id="chat-tab" class="tab-pane active"> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>User ID</label> | |
| <input type="text" id="chat-uid" placeholder="e.g. Abdullah123" value="TestUser"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Category</label> | |
| <input type="text" id="chat-cat" placeholder="e.g. Religious Self" value="General"> | |
| </div> | |
| </div> | |
| <div class="chat-window"> | |
| <div class="chat-header"> | |
| <span class="logo-moon" style="font-size:1rem">🌙</span> | |
| <span class="chat-title">Abdullah Bot</span> | |
| <span id="chat-model-info" style="font-family:'Space Mono',monospace;font-size:.62rem;color:var(--muted);margin-left:auto"></span> | |
| </div> | |
| <div class="chat-messages" id="chat-messages"> | |
| <div class="chat-placeholder" id="chat-placeholder">Assalamu alaikum. Type a message to begin your conversation…</div> | |
| </div> | |
| <div class="chat-input-area"> | |
| <input class="chat-input" type="text" id="chat-input" placeholder="Ask anything… or type 'start journey'" onkeydown="if(event.key==='Enter')sendChat()"> | |
| <button class="chat-send-btn" id="chat-send-btn" onclick="sendChat()">Send ↑</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- JOURNEY TAB --> | |
| <div id="journey-tab" class="tab-pane"> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>User ID</label> | |
| <input type="text" id="journey-uid" placeholder="e.g. Abdullah123" value="TestUser"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Category</label> | |
| <input type="text" id="journey-cat" placeholder="General" value="General"> | |
| </div> | |
| </div> | |
| <button class="run-btn" id="journey-run-btn" onclick="runJourney()"> | |
| <span id="journey-btn-icon">▶</span> Fetch Journey | |
| </button> | |
| <div class="response-area" id="journey-area" style="display:none"> | |
| <div class="response-header"> | |
| <span class="response-label">Response</span> | |
| <span class="status-pill" id="journey-status"></span> | |
| </div> | |
| <div class="response-out" id="journey-out"></div> | |
| </div> | |
| </div> | |
| <!-- CATEGORIES TAB --> | |
| <div id="cats-tab" class="tab-pane"> | |
| <p style="color:var(--muted);font-size:.88rem;margin-bottom:18px;">Fetch all available MCQ journey categories with question counts.</p> | |
| <button class="run-btn" id="cats-run-btn" onclick="runCats()"> | |
| <span id="cats-btn-icon">▶</span> Get Categories | |
| </button> | |
| <div class="response-area" id="cats-area" style="display:none"> | |
| <div class="response-header"> | |
| <span class="response-label">Response</span> | |
| <span class="status-pill" id="cats-status"></span> | |
| </div> | |
| <div class="response-out" id="cats-out"></div> | |
| </div> | |
| </div> | |
| <!-- HEALTH TAB --> | |
| <div id="health-tab2" class="tab-pane"> | |
| <p style="color:var(--muted);font-size:.88rem;margin-bottom:18px;">Verify API is running and check model configuration.</p> | |
| <button class="run-btn" id="health-run-btn2" onclick="runHealth()"> | |
| <span id="health-btn-icon2">▶</span> Check Health | |
| </button> | |
| <div class="response-area" id="health-area" style="display:none"> | |
| <div class="response-header"> | |
| <span class="response-label">Response</span> | |
| <span class="status-pill" id="health-status2"></span> | |
| </div> | |
| <div class="response-out" id="health-out2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </main> | |
| <footer> | |
| <div class="wrap"> | |
| <p>Enhanced Abdullah Bot API · Llama-3 · Supabase · FastAPI · | |
| <a href="/docs">Swagger Docs</a> · | |
| <a href="/health">Health</a> · | |
| <a href="/categories">Categories</a> · | |
| <a href="/documentation" style="color:#c9a84c">📖 Developer Docs</a> | |
| </p> | |
| </div> | |
| </footer> | |
| <script> | |
| const BASE = window.location.origin; | |
| function toggleCard(card) { | |
| const open = card.classList.contains('open'); | |
| document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('open')); | |
| if (!open) card.classList.add('open'); | |
| } | |
| function switchTab(id, btn) { | |
| document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById(id).classList.add('active'); | |
| btn.classList.add('active'); | |
| } | |
| function copyCode(btn) { | |
| const block = btn.parentElement; | |
| const text = block.innerText.replace(new RegExp('^copy\\n?'), '').trim(); | |
| navigator.clipboard.writeText(text).then(() => { | |
| btn.textContent = '✓ copied'; | |
| setTimeout(() => btn.textContent = 'copy', 1800); | |
| }); | |
| } | |
| function setBtn(id, iconId, loading) { | |
| document.getElementById(id).disabled = loading; | |
| document.getElementById(iconId).innerHTML = loading ? '<span class="spin"></span>' : '▶'; | |
| } | |
| function showResp(areaId, statusId, outId, status, data) { | |
| const area = document.getElementById(areaId); | |
| const statusEl = document.getElementById(statusId); | |
| area.style.display = 'block'; | |
| statusEl.className = 'status-pill ' + (status === 200 ? 'status-200' : 'status-err'); | |
| statusEl.textContent = status === 200 ? status + ' OK' : status + ' Error'; | |
| document.getElementById(outId).textContent = JSON.stringify(data, null, 2); | |
| } | |
| // ── CHAT ── | |
| let chatHistory = []; | |
| async function sendChat() { | |
| const message = document.getElementById('chat-input').value.trim(); | |
| const user_id = document.getElementById('chat-uid').value.trim() || 'TestUser'; | |
| const category = document.getElementById('chat-cat').value.trim() || 'General'; | |
| if (!message) return; | |
| document.getElementById('chat-placeholder')?.remove(); | |
| document.getElementById('chat-input').value = ''; | |
| document.getElementById('chat-send-btn').disabled = true; | |
| // Add user message | |
| addChatMsg('user', message, null, null, null, null); | |
| // Thinking bubble | |
| const thinkId = 'think-' + Date.now(); | |
| const box = document.getElementById('chat-messages'); | |
| const think = document.createElement('div'); | |
| think.className = 'msg bot'; | |
| think.id = thinkId; | |
| think.innerHTML = `<div class="msg-label">Abdullah</div><span style="color:var(--muted);font-style:italic">thinking…</span>`; | |
| box.appendChild(think); | |
| box.scrollTop = box.scrollHeight; | |
| try { | |
| const body = { message, user_id, category }; | |
| const res = await fetch(`${BASE}/chat`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body) | |
| }); | |
| const data = await res.json(); | |
| document.getElementById(thinkId)?.remove(); | |
| if (res.ok) { | |
| document.getElementById('chat-model-info').textContent = data.model_used ? `via ${data.model_used}` : ''; | |
| addChatMsg('bot', data.voice_answer, data.middle_section, data.middle_label, data.follow_up, data.model_used); | |
| } else { | |
| addChatMsg('bot', `Error: ${data.detail || 'Request failed'}`, null, null, null, null); | |
| } | |
| } catch(e) { | |
| document.getElementById(thinkId)?.remove(); | |
| addChatMsg('bot', `Network error: ${e.message}`, null, null, null, null); | |
| } | |
| document.getElementById('chat-send-btn').disabled = false; | |
| } | |
| function addChatMsg(role, voice, middle, middleLabel, followup, model) { | |
| const box = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg ' + role; | |
| let html = `<div class="msg-label">${role === 'user' ? 'You' : 'Abdullah'}</div>${escHtml(voice)}`; | |
| if (middle) { | |
| html += `<div class="msg-section"><div class="msg-section-label">${escHtml(middleLabel || 'Notes')}</div>${escHtml(middle)}</div>`; | |
| } | |
| if (followup) { | |
| html += `<div class="followup-text">↳ ${escHtml(followup)}</div>`; | |
| } | |
| if (model && role === 'bot') { | |
| html += `<div class="model-badge">via ${escHtml(model)}</div>`; | |
| } | |
| div.innerHTML = html; | |
| box.appendChild(div); | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| function escHtml(s) { | |
| if (!s) return ''; | |
| return String(s) | |
| .replace(new RegExp('&', 'g'), '&') | |
| .replace(new RegExp('<', 'g'), '<') | |
| .replace(new RegExp('>', 'g'), '>'); | |
| } | |
| // ── JOURNEY ── | |
| async function runJourney() { | |
| const uid = document.getElementById('journey-uid').value.trim() || 'TestUser'; | |
| const cat = document.getElementById('journey-cat').value.trim() || 'General'; | |
| setBtn('journey-run-btn','journey-btn-icon',true); | |
| try { | |
| const res = await fetch(`${BASE}/journey/${encodeURIComponent(uid)}?category=${encodeURIComponent(cat)}`); | |
| const data = await res.json(); | |
| showResp('journey-area','journey-status','journey-out', res.status, data); | |
| } catch(e) { | |
| showResp('journey-area','journey-status','journey-out', 500, {error:e.message}); | |
| } | |
| setBtn('journey-run-btn','journey-btn-icon',false); | |
| } | |
| // ── CATEGORIES ── | |
| async function runCats() { | |
| setBtn('cats-run-btn','cats-btn-icon',true); | |
| try { | |
| const res = await fetch(`${BASE}/categories`); | |
| const data = await res.json(); | |
| showResp('cats-area','cats-status','cats-out', res.status, data); | |
| } catch(e) { | |
| showResp('cats-area','cats-status','cats-out', 500, {error:e.message}); | |
| } | |
| setBtn('cats-run-btn','cats-btn-icon',false); | |
| } | |
| // ── HEALTH ── | |
| async function runHealth() { | |
| setBtn('health-run-btn2','health-btn-icon2',true); | |
| try { | |
| const res = await fetch(`${BASE}/health`); | |
| const data = await res.json(); | |
| showResp('health-area','health-status2','health-out2', res.status, data); | |
| } catch(e) { | |
| showResp('health-area','health-status2','health-out2', 500, {error:e.message}); | |
| } | |
| setBtn('health-run-btn2','health-btn-icon2',false); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def startup_event(): | |
| """Startup event""" | |
| logger.info("🚀 Enhanced Abdullah Bot API Starting...") | |
| logger.info(f"📡 Using HF Router API: {API_URL}") | |
| logger.info(f"🤖 Models configured: {', '.join([m['name'] for m in MODELS])}") | |
| logger.info(f"📊 MCQ Categories: {len(MCQ_BY_CAT)}") | |
| logger.info("بِسْمِ اللهِ الرَّحْمٰنِ الرَّحِيْمِ") | |
| async def shutdown_event(): | |
| """Cleanup on shutdown""" | |
| logger.info("👋 Shutting down Enhanced Abdullah Bot API...") | |
| logger.info("الْحَمْدُ لِلّٰهِ") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |