Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- Agent.py +1210 -0
- Dockerfile +35 -0
- Index.py +205 -0
- app.py +451 -0
- audio_processor.py +308 -0
- conversations.json +49 -0
- index.html +161 -0
- language_utils.py +28 -0
- rag_system.log +19 -0
- script.js +463 -0
- styles.css +1344 -0
Agent.py
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
+
import time
|
| 5 |
+
import pickle
|
| 6 |
+
import hashlib
|
| 7 |
+
import random
|
| 8 |
+
import re
|
| 9 |
+
from typing import List, Optional, Any
|
| 10 |
+
import logging
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
# === LANGUAGE DETECTION IMPORTS ===
|
| 15 |
+
from langdetect import detect, DetectorFactory
|
| 16 |
+
DetectorFactory.seed = 0
|
| 17 |
+
# ==================================
|
| 18 |
+
|
| 19 |
+
# === OPENAI CLIENT IMPORT ===
|
| 20 |
+
from openai import OpenAI
|
| 21 |
+
# ============================
|
| 22 |
+
|
| 23 |
+
# === HUGGING FACE DETECTION ===
|
| 24 |
+
IS_HUGGING_FACE = os.path.exists('/.dockerenv') or 'SPACE_ID' in os.environ
|
| 25 |
+
|
| 26 |
+
if IS_HUGGING_FACE:
|
| 27 |
+
print("🚀 Hugging Face Space detected")
|
| 28 |
+
os.environ['FORCE_FREE_MODEL'] = 'true'
|
| 29 |
+
|
| 30 |
+
# Load environment variables
|
| 31 |
+
if not IS_HUGGING_FACE:
|
| 32 |
+
env_path = os.path.join("config", ".env")
|
| 33 |
+
print(f"🔍 Looking for .env file at: {env_path}")
|
| 34 |
+
if os.path.exists(env_path):
|
| 35 |
+
load_dotenv(env_path)
|
| 36 |
+
print("✅ .env file loaded successfully")
|
| 37 |
+
else:
|
| 38 |
+
print(f"❌ .env file not found at: {env_path}")
|
| 39 |
+
else:
|
| 40 |
+
print("✅ Hugging Face environment - using repository secrets")
|
| 41 |
+
|
| 42 |
+
# === CACHE CONFIGURATION ===
|
| 43 |
+
CACHE_DIR = "cache"
|
| 44 |
+
RESPONSE_CACHE_FILE = os.path.join(CACHE_DIR, "response_cache.pkl")
|
| 45 |
+
|
| 46 |
+
class ResponseCache:
|
| 47 |
+
"""Simple response cache to avoid repeated LLM calls"""
|
| 48 |
+
|
| 49 |
+
def __init__(self):
|
| 50 |
+
self.cache = {}
|
| 51 |
+
self.load_cache()
|
| 52 |
+
|
| 53 |
+
def get_cache_key(self, query: str, context_chunks: List[Any]) -> str:
|
| 54 |
+
"""Generate unique cache key from query and context"""
|
| 55 |
+
query_hash = hashlib.md5(query.encode()).hexdigest()
|
| 56 |
+
if context_chunks:
|
| 57 |
+
# Use full context for better uniqueness
|
| 58 |
+
context_text = "".join([chunk.text for chunk in context_chunks if hasattr(chunk, 'text')])
|
| 59 |
+
context_hash = hashlib.md5(context_text.encode()).hexdigest()
|
| 60 |
+
else:
|
| 61 |
+
context_hash = "no_context"
|
| 62 |
+
return f"{query_hash}_{context_hash}"
|
| 63 |
+
|
| 64 |
+
def get(self, key: str) -> Optional[str]:
|
| 65 |
+
"""Get cached response if exists and not expired"""
|
| 66 |
+
if key in self.cache:
|
| 67 |
+
cached_time, response = self.cache[key]
|
| 68 |
+
if time.time() - cached_time < 24 * 3600:
|
| 69 |
+
return response
|
| 70 |
+
else:
|
| 71 |
+
del self.cache[key]
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
def set(self, key: str, response: str):
|
| 75 |
+
"""Cache response with timestamp"""
|
| 76 |
+
self.cache[key] = (time.time(), response)
|
| 77 |
+
self.save_cache()
|
| 78 |
+
|
| 79 |
+
def save_cache(self):
|
| 80 |
+
"""Save cache to disk"""
|
| 81 |
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 82 |
+
with open(RESPONSE_CACHE_FILE, 'wb') as f:
|
| 83 |
+
pickle.dump(self.cache, f)
|
| 84 |
+
|
| 85 |
+
def load_cache(self):
|
| 86 |
+
"""Load cache from disk"""
|
| 87 |
+
try:
|
| 88 |
+
if os.path.exists(RESPONSE_CACHE_FILE):
|
| 89 |
+
with open(RESPONSE_CACHE_FILE, 'rb') as f:
|
| 90 |
+
self.cache = pickle.load(f)
|
| 91 |
+
print(f"✅ Loaded response cache with {len(self.cache)} entries")
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"⚠️ Could not load cache: {e}")
|
| 94 |
+
self.cache = {}
|
| 95 |
+
|
| 96 |
+
# Initialize cache globally
|
| 97 |
+
response_cache = ResponseCache()
|
| 98 |
+
|
| 99 |
+
# === Conversation Logger ===
|
| 100 |
+
class ConversationLogger:
|
| 101 |
+
"""JSON-based conversation logging system"""
|
| 102 |
+
|
| 103 |
+
def __init__(self, log_file="conversations.json"):
|
| 104 |
+
self.log_file = log_file
|
| 105 |
+
self.ensure_log_file()
|
| 106 |
+
|
| 107 |
+
def ensure_log_file(self):
|
| 108 |
+
"""Ensure log file exists with proper structure"""
|
| 109 |
+
if not os.path.exists(self.log_file):
|
| 110 |
+
with open(self.log_file, 'w', encoding='utf-8') as f:
|
| 111 |
+
json.dump([], f, indent=2)
|
| 112 |
+
|
| 113 |
+
def log_conversation(self, user_input: str, llm_response: str, language: str, response_type: str):
|
| 114 |
+
"""Log conversation to JSON file"""
|
| 115 |
+
try:
|
| 116 |
+
# Read existing data
|
| 117 |
+
with open(self.log_file, 'r', encoding='utf-8') as f:
|
| 118 |
+
conversations = json.load(f)
|
| 119 |
+
|
| 120 |
+
# Add new conversation
|
| 121 |
+
conversation_entry = {
|
| 122 |
+
"timestamp": datetime.now().isoformat(),
|
| 123 |
+
"user_input": user_input,
|
| 124 |
+
"llm_response": llm_response,
|
| 125 |
+
"language": language,
|
| 126 |
+
"response_type": response_type
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
conversations.append(conversation_entry)
|
| 130 |
+
|
| 131 |
+
# Write back to file
|
| 132 |
+
with open(self.log_file, 'w', encoding='utf-8') as f:
|
| 133 |
+
json.dump(conversations, f, indent=2, ensure_ascii=False)
|
| 134 |
+
|
| 135 |
+
print(f"💾 Conversation logged to {self.log_file}")
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"❌ Error logging conversation: {e}")
|
| 139 |
+
|
| 140 |
+
# Initialize global logger
|
| 141 |
+
conversation_logger = ConversationLogger()
|
| 142 |
+
|
| 143 |
+
# === Centralized Configuration System ===
|
| 144 |
+
class Config:
|
| 145 |
+
"""Centralized configuration - loads ONLY from config/config.json for all environments"""
|
| 146 |
+
|
| 147 |
+
def __init__(self):
|
| 148 |
+
self.api_keys = []
|
| 149 |
+
self.current_key_index = 0
|
| 150 |
+
self.settings = self._load_config_file()
|
| 151 |
+
self._validate_and_correct_paths()
|
| 152 |
+
|
| 153 |
+
self.SUPPORTED_LANGUAGES = ["english", "urdu"]
|
| 154 |
+
self.DEFAULT_LANGUAGE = "english"
|
| 155 |
+
|
| 156 |
+
# Apply settings
|
| 157 |
+
self.MODEL_PROVIDER = self.settings["model_provider"]
|
| 158 |
+
self.MODEL_ID = self.settings["model_id"]
|
| 159 |
+
self.API_KEYS_FOLDER = self.settings["api_keys_folder"]
|
| 160 |
+
self.INDEX_PATH = self.settings["index_path"]
|
| 161 |
+
self.DATASET_PATH = self.settings["dataset_path"]
|
| 162 |
+
self.SIMILARITY_TOP_K = self.settings["similarity_top_k"]
|
| 163 |
+
self.TEMPERATURE = self.settings["temperature"]
|
| 164 |
+
self.MAX_TOKENS = self.settings["max_tokens"]
|
| 165 |
+
self.FALLBACK_MESSAGE = self.settings["fallback_message"]
|
| 166 |
+
|
| 167 |
+
self.api_keys = self._load_api_keys()
|
| 168 |
+
self.api_key = self._get_current_api_key()
|
| 169 |
+
|
| 170 |
+
self._validate_config()
|
| 171 |
+
|
| 172 |
+
def _load_config_file(self):
|
| 173 |
+
"""Load configuration ONLY from config/config.json file for ALL environments"""
|
| 174 |
+
config_file = os.path.join("config", "config.json")
|
| 175 |
+
|
| 176 |
+
# Default configuration as fallback
|
| 177 |
+
default_config = {
|
| 178 |
+
"model_provider": "openrouter",
|
| 179 |
+
"model_id": "meta-llama/llama-3.3-70b-instruct:free",
|
| 180 |
+
"api_keys_folder": "config",
|
| 181 |
+
"index_path": "cancer_index_store",
|
| 182 |
+
"dataset_path": "breast_cancer.json",
|
| 183 |
+
"similarity_top_k": 5,
|
| 184 |
+
"temperature": 0.2,
|
| 185 |
+
"max_tokens": 350,
|
| 186 |
+
"fallback_message": "Sorry, I don't know the answer."
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
if os.path.exists(config_file):
|
| 191 |
+
print(f"✅ Loading configuration from: {config_file}")
|
| 192 |
+
with open(config_file, 'r', encoding='utf-8') as f:
|
| 193 |
+
loaded_config = json.load(f)
|
| 194 |
+
|
| 195 |
+
# Merge with defaults for missing keys
|
| 196 |
+
final_config = {**default_config, **loaded_config}
|
| 197 |
+
|
| 198 |
+
print("📋 Configuration loaded successfully from config.json")
|
| 199 |
+
return final_config
|
| 200 |
+
else:
|
| 201 |
+
# Create directory and config file if it doesn't exist
|
| 202 |
+
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
| 203 |
+
with open(config_file, 'w', encoding='utf-8') as f:
|
| 204 |
+
json.dump(default_config, f, indent=4)
|
| 205 |
+
print(f"📁 Created default config file at: {config_file}")
|
| 206 |
+
return default_config
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
print(f"❌ Error loading config from {config_file}: {e}")
|
| 210 |
+
print("🔄 Using default configuration as fallback")
|
| 211 |
+
return default_config
|
| 212 |
+
|
| 213 |
+
def _validate_and_correct_paths(self):
|
| 214 |
+
"""Validate and correct file paths"""
|
| 215 |
+
# Correct dataset path if needed
|
| 216 |
+
original_dataset_path = self.settings["dataset_path"]
|
| 217 |
+
if not os.path.exists(original_dataset_path):
|
| 218 |
+
possible_paths = [
|
| 219 |
+
original_dataset_path,
|
| 220 |
+
f"DataSet/{original_dataset_path}",
|
| 221 |
+
f"data/{original_dataset_path}",
|
| 222 |
+
"DataSet/breast_cancer.json",
|
| 223 |
+
"breast_cancer.json"
|
| 224 |
+
]
|
| 225 |
+
|
| 226 |
+
for path in possible_paths:
|
| 227 |
+
if os.path.exists(path):
|
| 228 |
+
if path != original_dataset_path:
|
| 229 |
+
print(f"🔄 Using dataset at: {path}")
|
| 230 |
+
self.settings["dataset_path"] = path
|
| 231 |
+
else:
|
| 232 |
+
print(f"✅ Dataset found at: {path}")
|
| 233 |
+
return
|
| 234 |
+
|
| 235 |
+
print(f"❌ Dataset not found in any location")
|
| 236 |
+
else:
|
| 237 |
+
print(f"✅ Dataset found at: {original_dataset_path}")
|
| 238 |
+
|
| 239 |
+
def _load_api_keys(self) -> List[str]:
|
| 240 |
+
"""Load API keys from environment variables"""
|
| 241 |
+
api_keys = []
|
| 242 |
+
print("🔍 Checking for API keys in environment variables...")
|
| 243 |
+
|
| 244 |
+
keys_to_check = ["API_KEY", "API_KEY_2", "API_KEY_3", "API_KEY_4", "API_KEY_5"]
|
| 245 |
+
|
| 246 |
+
for key_name in keys_to_check:
|
| 247 |
+
key_value = os.getenv(key_name)
|
| 248 |
+
if key_value and key_value.strip():
|
| 249 |
+
api_keys.append(key_value.strip())
|
| 250 |
+
print(f"✅ Found {key_name}")
|
| 251 |
+
|
| 252 |
+
return api_keys
|
| 253 |
+
|
| 254 |
+
def _get_current_api_key(self) -> str:
|
| 255 |
+
"""Get current active API key"""
|
| 256 |
+
if self.api_keys and self.current_key_index < len(self.api_keys):
|
| 257 |
+
return self.api_keys[self.current_key_index]
|
| 258 |
+
return ""
|
| 259 |
+
|
| 260 |
+
def rotate_to_next_key(self) -> bool:
|
| 261 |
+
"""Rotate to next API key if available"""
|
| 262 |
+
if self.current_key_index < len(self.api_keys) - 1:
|
| 263 |
+
self.current_key_index += 1
|
| 264 |
+
self.api_key = self._get_current_api_key()
|
| 265 |
+
print(f"🔄 Rotated to API key {self.current_key_index + 1}")
|
| 266 |
+
return True
|
| 267 |
+
else:
|
| 268 |
+
print("❌ No more API keys available")
|
| 269 |
+
return False
|
| 270 |
+
|
| 271 |
+
def _validate_config(self):
|
| 272 |
+
"""Validate configuration"""
|
| 273 |
+
if not self.api_keys:
|
| 274 |
+
print("❌ No API keys found in environment variables")
|
| 275 |
+
if IS_HUGGING_FACE:
|
| 276 |
+
print("💡 Please add API keys in Hugging Face Space Settings → Repository secrets")
|
| 277 |
+
else:
|
| 278 |
+
print(f"✅ Found {len(self.api_keys)} API key(s)")
|
| 279 |
+
|
| 280 |
+
# Print current configuration
|
| 281 |
+
print("📋 Current Configuration (from config.json):")
|
| 282 |
+
print(f" Model Provider: {self.MODEL_PROVIDER}")
|
| 283 |
+
print(f" Model ID: {self.MODEL_ID}")
|
| 284 |
+
print(f" Index Path: {self.INDEX_PATH}")
|
| 285 |
+
print(f" Dataset Path: {self.DATASET_PATH}")
|
| 286 |
+
print(f" Similarity Top K: {self.SIMILARITY_TOP_K}")
|
| 287 |
+
print(f" Temperature: {self.TEMPERATURE}")
|
| 288 |
+
print(f" Max Tokens: {self.MAX_TOKENS}")
|
| 289 |
+
|
| 290 |
+
# Initialize configuration
|
| 291 |
+
config = Config()
|
| 292 |
+
|
| 293 |
+
# === Setup Logging ===
|
| 294 |
+
if IS_HUGGING_FACE:
|
| 295 |
+
logging.basicConfig(
|
| 296 |
+
level=logging.INFO,
|
| 297 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 298 |
+
handlers=[logging.StreamHandler()]
|
| 299 |
+
)
|
| 300 |
+
else:
|
| 301 |
+
logging.basicConfig(
|
| 302 |
+
level=logging.INFO,
|
| 303 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 304 |
+
handlers=[
|
| 305 |
+
logging.StreamHandler(),
|
| 306 |
+
logging.FileHandler('rag_system.log')
|
| 307 |
+
]
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# === FAST INDEX LOADING ===
|
| 311 |
+
def load_index_fast():
|
| 312 |
+
"""Fast index loading by reusing cached embeddings"""
|
| 313 |
+
try:
|
| 314 |
+
from llama_index.core import StorageContext, VectorStoreIndex
|
| 315 |
+
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 316 |
+
|
| 317 |
+
print(f"🔍 Loading index from: {config.INDEX_PATH}")
|
| 318 |
+
if not os.path.exists(config.INDEX_PATH):
|
| 319 |
+
print(f"❌ Index path doesn't exist: {config.INDEX_PATH}")
|
| 320 |
+
return None, None
|
| 321 |
+
|
| 322 |
+
embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
| 323 |
+
storage_context = StorageContext.from_defaults(persist_dir=config.INDEX_PATH)
|
| 324 |
+
index = VectorStoreIndex.from_documents(
|
| 325 |
+
[],
|
| 326 |
+
storage_context=storage_context,
|
| 327 |
+
embed_model=embed_model
|
| 328 |
+
)
|
| 329 |
+
retriever = index.as_retriever(similarity_top_k=config.SIMILARITY_TOP_K)
|
| 330 |
+
print("✅ Index loaded successfully")
|
| 331 |
+
return index, retriever
|
| 332 |
+
|
| 333 |
+
except Exception as e:
|
| 334 |
+
print(f"❌ Failed to load index: {e}")
|
| 335 |
+
import traceback
|
| 336 |
+
traceback.print_exc()
|
| 337 |
+
return None, None
|
| 338 |
+
|
| 339 |
+
def load_index():
|
| 340 |
+
return load_index_fast()
|
| 341 |
+
|
| 342 |
+
# === Enhanced RAG System Class ===
|
| 343 |
+
class BreastCancerRAGSystem:
|
| 344 |
+
"""Enhanced RAG system for breast cancer information with emotional support"""
|
| 345 |
+
|
| 346 |
+
def __init__(self, index, retriever):
|
| 347 |
+
self.index = index
|
| 348 |
+
self.retriever = retriever
|
| 349 |
+
self.conversation_history = []
|
| 350 |
+
|
| 351 |
+
if not config.api_keys:
|
| 352 |
+
logging.error("🚫 System initialized without API key - LLM features will not work")
|
| 353 |
+
|
| 354 |
+
def get_predefined_questions(self, language: str = "english") -> List[dict]:
|
| 355 |
+
"""Get predefined daily routine questions for breast cancer patients"""
|
| 356 |
+
|
| 357 |
+
english_questions = [
|
| 358 |
+
{
|
| 359 |
+
"question": "What are some gentle exercises I can do during recovery?",
|
| 360 |
+
"category": "exercise",
|
| 361 |
+
"icon": "fas fa-walking"
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"question": "How do I deal with anxiety about my next treatment?",
|
| 365 |
+
"category": "emotional",
|
| 366 |
+
"icon": "fas fa-heart"
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"question": "When can I expect my hair to grow back after treatment?",
|
| 370 |
+
"category": "appearance",
|
| 371 |
+
"icon": "fas fa-user"
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"question": "How do I talk to my family about my diagnosis?",
|
| 375 |
+
"category": "emotional",
|
| 376 |
+
"icon": "fas fa-users"
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"question": "What are the signs of infection I should watch for?",
|
| 380 |
+
"category": "symptoms",
|
| 381 |
+
"icon": "fas fa-exclamation-triangle"
|
| 382 |
+
}
|
| 383 |
+
]
|
| 384 |
+
|
| 385 |
+
urdu_questions = [
|
| 386 |
+
{
|
| 387 |
+
"question": "کیموتھراپی کے دوران تھکاوٹ کیسے کم کریں؟",
|
| 388 |
+
"category": "symptoms",
|
| 389 |
+
"icon": "fas fa-bed"
|
| 390 |
+
},
|
| 391 |
+
{
|
| 392 |
+
"question": "ریکوری کے دوران ہلکی پھلکی ورزشیں کون سی ہیں؟",
|
| 393 |
+
"category": "exercise",
|
| 394 |
+
"icon": "fas fa-walking"
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"question": "اگلے علاج کے بارے میں پریشانی کیسے دور کریں؟",
|
| 398 |
+
"category": "emotional",
|
| 399 |
+
"icon": "fas fa-heart"
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"question": "کیموتھراپی کے بعد متلی کے لیے کون سی غذائیں مفید ہیں؟",
|
| 403 |
+
"category": "nutrition",
|
| 404 |
+
"icon": "fas fa-apple-alt"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"question": "ماسٹکٹومی کے بعد درد کیسے منظم کریں؟",
|
| 408 |
+
"category": "pain",
|
| 409 |
+
"icon": "fas fa-hand-holding-heart"
|
| 410 |
+
},
|
| 411 |
+
]
|
| 412 |
+
|
| 413 |
+
return urdu_questions if language == "urdu" else english_questions
|
| 414 |
+
|
| 415 |
+
def detect_language(self, text: str) -> str:
|
| 416 |
+
"""Detect language of user query"""
|
| 417 |
+
try:
|
| 418 |
+
urdu_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+')
|
| 419 |
+
if urdu_pattern.search(text):
|
| 420 |
+
return 'urdu'
|
| 421 |
+
detected_lang = detect(text)
|
| 422 |
+
return 'urdu' if detected_lang == 'ur' else 'english'
|
| 423 |
+
except:
|
| 424 |
+
return 'english'
|
| 425 |
+
|
| 426 |
+
def _clean_urdu_text(self, text: str) -> str:
|
| 427 |
+
"""Advanced cleaning for Urdu text with comprehensive spelling correction"""
|
| 428 |
+
if not text or not text.strip():
|
| 429 |
+
return text
|
| 430 |
+
|
| 431 |
+
# Comprehensive spelling correction dictionary
|
| 432 |
+
spelling_corrections = {
|
| 433 |
+
# Character repetition fixes
|
| 434 |
+
'مجہے': 'مجھے',
|
| 435 |
+
'پروگرہوں': 'پروگرام',
|
| 436 |
+
'کہےنسر': 'کینسر',
|
| 437 |
+
'ڈڈاکٹر': 'ڈاکٹر',
|
| 438 |
+
'ہےہ': 'ہے',
|
| 439 |
+
'مہےں': 'میں',
|
| 440 |
+
'ہےں': 'ہیں',
|
| 441 |
+
'ھے': 'ہے',
|
| 442 |
+
'ھوں': 'ہوں',
|
| 443 |
+
'ھیں': 'ہیں',
|
| 444 |
+
'ےے': 'ے',
|
| 445 |
+
'ںں': 'ں',
|
| 446 |
+
'ہہ': 'ہ',
|
| 447 |
+
'یی': 'ی',
|
| 448 |
+
|
| 449 |
+
# Common phrase corrections
|
| 450 |
+
'ے لہےے': 'کے لیے',
|
| 451 |
+
'کا ے لہےے': 'کے لیے',
|
| 452 |
+
'و ہےہ': 'کو',
|
| 453 |
+
'ہےقہےن': 'یقین',
|
| 454 |
+
'اکہےلے': 'اکیلے',
|
| 455 |
+
'نہہےں': 'نہیں',
|
| 456 |
+
'ہہےں': 'ہیں',
|
| 457 |
+
'کا ے': 'کے',
|
| 458 |
+
'ساتھ ہہےں': 'ساتھ ہیں',
|
| 459 |
+
'تجوہےز': 'تجویز',
|
| 460 |
+
'ضرورہے': 'ضروری',
|
| 461 |
+
'بارے مہےں': 'بارے میں',
|
| 462 |
+
'کرہےں': 'کریں',
|
| 463 |
+
'بہترہےن': 'بہترین',
|
| 464 |
+
'ہے مدد': 'کی مدد',
|
| 465 |
+
'خوشہے': 'خوشی',
|
| 466 |
+
'ترجہےح': 'ترجیح',
|
| 467 |
+
'جسے سے': 'جس سے',
|
| 468 |
+
|
| 469 |
+
# Medical term corrections
|
| 470 |
+
'برہےسٹ': 'بریسٹ',
|
| 471 |
+
'کہےموتھراپہے': 'کیموتھراپی',
|
| 472 |
+
'متلہے': 'متلی',
|
| 473 |
+
'غذائہےں': 'غذائیں',
|
| 474 |
+
'چربہے': 'چربی',
|
| 475 |
+
'ہلکے': 'ہلکی',
|
| 476 |
+
'آسانہے': 'آسانی',
|
| 477 |
+
'ہائہےڈرہےٹنگ': 'ہائیڈریٹنگ',
|
| 478 |
+
'ہائہےڈرہےٹڈ': 'ہائیڈریٹڈ',
|
| 479 |
+
|
| 480 |
+
# Grammar and structure fixes
|
| 481 |
+
'کرنےے': 'کرنے',
|
| 482 |
+
'ہونےے': 'ہونے',
|
| 483 |
+
'سکتےے': 'سکتے',
|
| 484 |
+
'سکتیی': 'سکتی',
|
| 485 |
+
'والےے': 'والے',
|
| 486 |
+
'والیی': 'والی',
|
| 487 |
+
'کہے': 'کے',
|
| 488 |
+
'ہےے': 'ہے',
|
| 489 |
+
|
| 490 |
+
# Common word fixes
|
| 491 |
+
'ام ': 'ہوں ',
|
| 492 |
+
'می ': 'میں ',
|
| 493 |
+
'آپ ک': 'آپ کا ',
|
| 494 |
+
'دوران ': 'دوران ',
|
| 495 |
+
'عام ': 'عام ',
|
| 496 |
+
'مسئل ': 'مسئلہ ',
|
| 497 |
+
'اس ': 'اس ',
|
| 498 |
+
'کو ': 'کو ',
|
| 499 |
+
'کرن ': 'کرنے ',
|
| 500 |
+
'س ': 'سے ',
|
| 501 |
+
'طریق ': 'طریقے ',
|
| 502 |
+
'بتا ': 'بتا ',
|
| 503 |
+
'سکتی ': 'سکتی ',
|
| 504 |
+
'اکٹر': 'ڈاکٹر',
|
| 505 |
+
'اکیل': 'اکیلے',
|
| 506 |
+
'میش': 'میں',
|
| 507 |
+
'وتی': 'ہوتی',
|
| 508 |
+
'لکی': 'ہلکی',
|
| 509 |
+
'بتر': 'بہتر',
|
| 510 |
+
'محفوظ ر': 'محفوظ رکھتی ہے',
|
| 511 |
+
'رشت': 'رشتہ داروں',
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
# Apply spelling corrections iteratively
|
| 515 |
+
cleaned_text = text
|
| 516 |
+
for wrong, correct in spelling_corrections.items():
|
| 517 |
+
cleaned_text = cleaned_text.replace(wrong, correct)
|
| 518 |
+
|
| 519 |
+
# Fix common grammatical patterns using regex for better coverage
|
| 520 |
+
import re
|
| 521 |
+
|
| 522 |
+
# Fix character repetition patterns
|
| 523 |
+
repetition_patterns = [
|
| 524 |
+
(r'ہہ', 'ہ'),
|
| 525 |
+
(r'یی', 'ی'),
|
| 526 |
+
(r'ےے', 'ے'),
|
| 527 |
+
(r'ںں', 'ں'),
|
| 528 |
+
(r'کک', 'ک'),
|
| 529 |
+
(r'گگ', 'گ'),
|
| 530 |
+
]
|
| 531 |
+
|
| 532 |
+
for pattern, replacement in repetition_patterns:
|
| 533 |
+
cleaned_text = re.sub(pattern, replacement, cleaned_text)
|
| 534 |
+
|
| 535 |
+
# Fix common grammatical patterns
|
| 536 |
+
grammatical_fixes = [
|
| 537 |
+
('ک دوران', 'کے دوران'),
|
| 538 |
+
('ک بار', 'کے بارے'),
|
| 539 |
+
('ک بعد', 'کے بعد'),
|
| 540 |
+
('ک لی', 'کے لیے'),
|
| 541 |
+
('ک ساتھ', 'کے ساتھ'),
|
| 542 |
+
('ک طور', 'کے طور'),
|
| 543 |
+
('ک ذریع', 'کے ذریعے'),
|
| 544 |
+
('ک مطابق', 'کے مطابق'),
|
| 545 |
+
]
|
| 546 |
+
|
| 547 |
+
for wrong, correct in grammatical_fixes:
|
| 548 |
+
cleaned_text = cleaned_text.replace(wrong, correct)
|
| 549 |
+
|
| 550 |
+
# Fix spacing and punctuation issues
|
| 551 |
+
cleaned_text = re.sub(r'\s+', ' ', cleaned_text) # Multiple spaces to single space
|
| 552 |
+
cleaned_text = re.sub(r' \.', '.', cleaned_text) # Space before period
|
| 553 |
+
cleaned_text = re.sub(r' ،', '،', cleaned_text) # Space before comma
|
| 554 |
+
cleaned_text = re.sub(r' ', ' ', cleaned_text) # Double spaces
|
| 555 |
+
cleaned_text = re.sub(r'۔۔', '۔', cleaned_text) # Double periods
|
| 556 |
+
|
| 557 |
+
# Ensure sentence completion and structure
|
| 558 |
+
sentences = cleaned_text.split('۔')
|
| 559 |
+
cleaned_sentences = []
|
| 560 |
+
|
| 561 |
+
for sentence in sentences:
|
| 562 |
+
sentence = sentence.strip()
|
| 563 |
+
if sentence and len(sentence.split()) >= 2: # At least 2 words
|
| 564 |
+
# Ensure sentence starts properly (no hanging characters)
|
| 565 |
+
if sentence and sentence[0] in [' ', '،', '۔']:
|
| 566 |
+
sentence = sentence[1:].strip()
|
| 567 |
+
if sentence:
|
| 568 |
+
cleaned_sentences.append(sentence)
|
| 569 |
+
|
| 570 |
+
# Reconstruct text with proper punctuation
|
| 571 |
+
if cleaned_sentences:
|
| 572 |
+
cleaned_text = '۔ '.join(cleaned_sentences) + '۔'
|
| 573 |
+
else:
|
| 574 |
+
cleaned_text = cleaned_text.strip()
|
| 575 |
+
|
| 576 |
+
# Final normalization
|
| 577 |
+
cleaned_text = cleaned_text.strip()
|
| 578 |
+
|
| 579 |
+
return cleaned_text
|
| 580 |
+
|
| 581 |
+
def _detect_emotional_needs(self, user_query: str, language: str = "english") -> dict:
|
| 582 |
+
"""Enhanced emotional need detection with better Urdu support"""
|
| 583 |
+
query_lower = user_query.lower()
|
| 584 |
+
|
| 585 |
+
# Emotional triggers in both languages
|
| 586 |
+
emotional_triggers_english = [
|
| 587 |
+
"scared", "afraid", "worried", "anxious", "fear", "nervous", "stressed",
|
| 588 |
+
"overwhelmed", "depressed", "sad", "lonely", "alone", "hopeless",
|
| 589 |
+
"can't cope", "struggling", "difficult", "hard time", "suffering",
|
| 590 |
+
"terrified", "panic", "breakdown", "crying", "tears", "misery"
|
| 591 |
+
]
|
| 592 |
+
|
| 593 |
+
emotional_triggers_urdu = [
|
| 594 |
+
"خوف", "ڈر", "پریشانی", "فکر", "تنہائی", "اداسی", "مایوسی", "تکلیف",
|
| 595 |
+
"گھبراہٹ", "بے چینی", "بے بسی", "رونا", "آنسو", "دکھ", "غم",
|
| 596 |
+
"ہمت", "طاقت", "حوصلہ", "پرسکون", "سکون", "چین"
|
| 597 |
+
]
|
| 598 |
+
|
| 599 |
+
# Information triggers
|
| 600 |
+
info_triggers_english = [
|
| 601 |
+
"what", "how", "when", "where", "which", "why",
|
| 602 |
+
"treatment", "medication", "exercise", "diet", "symptoms",
|
| 603 |
+
"pain", "side effects", "recovery", "diagnosis", "procedure"
|
| 604 |
+
]
|
| 605 |
+
|
| 606 |
+
info_triggers_urdu = [
|
| 607 |
+
"کیا", "کیسے", "کب", "کہاں", "کون سا", "کیوں", "کس طرح",
|
| 608 |
+
"علاج", "دوا", "ورزش", "غذا", "علامات", "درد", "مراحل",
|
| 609 |
+
"طریقہ", "عمل", "تفصیل", "معلومات"
|
| 610 |
+
]
|
| 611 |
+
|
| 612 |
+
if language == "urdu":
|
| 613 |
+
emotional_triggers = emotional_triggers_urdu
|
| 614 |
+
info_triggers = info_triggers_urdu
|
| 615 |
+
else:
|
| 616 |
+
emotional_triggers = emotional_triggers_english
|
| 617 |
+
info_triggers = info_triggers_english
|
| 618 |
+
|
| 619 |
+
# More sophisticated emotional detection
|
| 620 |
+
emotional_score = 0
|
| 621 |
+
for trigger in emotional_triggers:
|
| 622 |
+
if trigger in query_lower:
|
| 623 |
+
emotional_score += 1
|
| 624 |
+
|
| 625 |
+
# Context-aware emotional detection
|
| 626 |
+
negative_context_words = ["not", "don't", "no", "never", "n't"]
|
| 627 |
+
has_negative_context = any(word in query_lower for word in negative_context_words)
|
| 628 |
+
|
| 629 |
+
info_score = sum(1 for trigger in info_triggers if trigger in query_lower)
|
| 630 |
+
|
| 631 |
+
return {
|
| 632 |
+
"needs_emotional_support": emotional_score > 0 and not has_negative_context,
|
| 633 |
+
"needs_information": info_score > 0,
|
| 634 |
+
"emotional_score": emotional_score,
|
| 635 |
+
"info_score": info_score
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
def _add_emotional_support(self, response: str, user_query: str, language: str = "english") -> str:
|
| 639 |
+
"""Add natural emotional support integrated into the response"""
|
| 640 |
+
emotional_needs = self._detect_emotional_needs(user_query, language)
|
| 641 |
+
|
| 642 |
+
# Always add some level of emotional support, but more if detected
|
| 643 |
+
if language == "urdu":
|
| 644 |
+
if emotional_needs["needs_emotional_support"]:
|
| 645 |
+
# Strong emotional support phrases
|
| 646 |
+
support_phrases = [
|
| 647 |
+
"آپ کی طاقت قابلِ تعریف ہے، اور میں آپ کے ساتھ ہوں۔",
|
| 648 |
+
"یہ مشکل وقت ہے، لیکن آپ اکیلے نہیں ہیں۔ ہم مل کر اس کا سامنا کریں گے۔",
|
| 649 |
+
"آپ کی ہمت اور صبر کو سلام، بہتر دن ضرور آئیں گے۔",
|
| 650 |
+
]
|
| 651 |
+
else:
|
| 652 |
+
# Gentle emotional support phrases
|
| 653 |
+
support_phrases = [
|
| 654 |
+
"آپ کی صحت اور خوشی ہماری پہلی ترجیح ہے۔",
|
| 655 |
+
"یقین رکھیں، ہر طوفان کے بعد سکون ضرور آتا ہے۔",
|
| 656 |
+
"آپ جیسے بہادر لوگ ہی دنیا کو روشن کرتے ہیں۔",
|
| 657 |
+
]
|
| 658 |
+
else:
|
| 659 |
+
if emotional_needs["needs_emotional_support"]:
|
| 660 |
+
# Strong emotional support phrases
|
| 661 |
+
support_phrases = [
|
| 662 |
+
"Your strength is truly admirable, and I'm here with you every step of the way.",
|
| 663 |
+
"This is a challenging time, but you're not alone. We'll face this together.",
|
| 664 |
+
"I want you to know how much courage you're showing, and better days will come.",
|
| 665 |
+
]
|
| 666 |
+
else:
|
| 667 |
+
# Gentle emotional support phrases
|
| 668 |
+
support_phrases = [
|
| 669 |
+
"Your wellbeing and happiness are my top priority right now.",
|
| 670 |
+
"Please remember that after every storm comes calm.",
|
| 671 |
+
"People like you, with such resilience, truly light up the world.",
|
| 672 |
+
]
|
| 673 |
+
|
| 674 |
+
# Choose a support phrase that fits naturally
|
| 675 |
+
support_text = random.choice(support_phrases)
|
| 676 |
+
|
| 677 |
+
# Integrate support naturally - for Urdu, place at beginning for impact
|
| 678 |
+
if language == "urdu":
|
| 679 |
+
if support_text not in response:
|
| 680 |
+
# Check if response already has emotional content
|
| 681 |
+
if not any(phrase in response for phrase in ['طاقت', 'ہمت', 'حوصلہ', 'سکون', 'خوشی']):
|
| 682 |
+
return f"{support_text}\n\n{response}"
|
| 683 |
+
else:
|
| 684 |
+
if support_text not in response:
|
| 685 |
+
# Check if response already has emotional content
|
| 686 |
+
if not any(phrase in response for phrase in ['strength', 'courage', 'hope', 'together', 'proud']):
|
| 687 |
+
return f"{support_text}\n\n{response}"
|
| 688 |
+
|
| 689 |
+
return response
|
| 690 |
+
|
| 691 |
+
def retrieve_relevant_chunks(self, user_query: str, language: str = "english") -> List[Any]:
|
| 692 |
+
"""Retrieve relevant chunks with language-specific prioritization"""
|
| 693 |
+
if not hasattr(self, 'retriever') or self.retriever is None:
|
| 694 |
+
print("❌ Retriever not available")
|
| 695 |
+
return []
|
| 696 |
+
|
| 697 |
+
try:
|
| 698 |
+
if language == "urdu":
|
| 699 |
+
print("🔍 Prioritizing Urdu content for Urdu query...")
|
| 700 |
+
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters
|
| 701 |
+
urdu_filter = MetadataFilter(key="language", value="urdu")
|
| 702 |
+
urdu_results = self.retriever.retrieve(
|
| 703 |
+
user_query,
|
| 704 |
+
filters=MetadataFilters(filters=[urdu_filter])
|
| 705 |
+
)
|
| 706 |
+
|
| 707 |
+
quality_threshold = 0.5
|
| 708 |
+
high_quality_urdu = [
|
| 709 |
+
result for result in urdu_results
|
| 710 |
+
if hasattr(result, 'score') and result.score >= quality_threshold
|
| 711 |
+
]
|
| 712 |
+
|
| 713 |
+
if high_quality_urdu:
|
| 714 |
+
print(f"✅ Found {len(high_quality_urdu)} high-quality Urdu chunks")
|
| 715 |
+
return high_quality_urdu[:5]
|
| 716 |
+
elif urdu_results:
|
| 717 |
+
print(f"⚠️ Using {len(urdu_results)} lower-confidence Urdu chunks")
|
| 718 |
+
return urdu_results[:3]
|
| 719 |
+
|
| 720 |
+
print("🔍 No Urdu content found, searching all content...")
|
| 721 |
+
|
| 722 |
+
retrieval_results = self.retriever.retrieve(user_query)
|
| 723 |
+
quality_threshold = 0.5
|
| 724 |
+
high_quality_results = [
|
| 725 |
+
result for result in retrieval_results
|
| 726 |
+
if hasattr(result, 'score') and result.score >= quality_threshold
|
| 727 |
+
]
|
| 728 |
+
|
| 729 |
+
if not high_quality_results and retrieval_results:
|
| 730 |
+
print("⚠️ Using lower confidence results")
|
| 731 |
+
return retrieval_results[:3]
|
| 732 |
+
|
| 733 |
+
print(f"✅ Retrieved {len(high_quality_results)} relevant chunks")
|
| 734 |
+
return high_quality_results[:5]
|
| 735 |
+
|
| 736 |
+
except Exception as e:
|
| 737 |
+
print(f"❌ Retrieval error: {e}")
|
| 738 |
+
return []
|
| 739 |
+
|
| 740 |
+
def build_concise_prompt(self, user_query: str, context_chunks: List[Any], language: str = "english") -> str:
|
| 741 |
+
"""Build prompt for concise, targeted responses with emotional intelligence"""
|
| 742 |
+
|
| 743 |
+
context_text = ""
|
| 744 |
+
if context_chunks:
|
| 745 |
+
context_parts = []
|
| 746 |
+
for i, chunk in enumerate(context_chunks[:2]):
|
| 747 |
+
chunk_text = chunk.text if hasattr(chunk, 'text') else str(chunk)
|
| 748 |
+
key_points = " ".join(chunk_text.split()[:100])
|
| 749 |
+
context_parts.append(f"CONTEXT {i+1}: {key_points}")
|
| 750 |
+
context_text = "\n".join(context_parts)
|
| 751 |
+
|
| 752 |
+
# Analyze emotional and information needs
|
| 753 |
+
needs_analysis = self._detect_emotional_needs(user_query, language)
|
| 754 |
+
|
| 755 |
+
if language == "urdu":
|
| 756 |
+
prompt = f"""
|
| 757 |
+
# WELL BEING AGENT - BREAST CANCER SUPPORT
|
| 758 |
+
# CRITICAL: RESPOND ONLY IN URDU LANGUAGE USING CORRECT URDU SPELLING AND GRAMMAR
|
| 759 |
+
# ABSOLUTELY NO HINDI, ARABIC, OR OTHER LANGUAGES - PURE URDU ONLY
|
| 760 |
+
|
| 761 |
+
## PATIENT'S QUERY:
|
| 762 |
+
"{user_query}"
|
| 763 |
+
|
| 764 |
+
## EMOTIONAL ANALYSIS:
|
| 765 |
+
- Needs Emotional Support: {'YES' if needs_analysis['needs_emotional_support'] else 'NO'}
|
| 766 |
+
- Needs Information: {'YES' if needs_analysis['needs_information'] else 'NO'}
|
| 767 |
+
|
| 768 |
+
## CONTEXT (USE IF RELEVANT):
|
| 769 |
+
{context_text if context_text else "General breast cancer knowledge"}
|
| 770 |
+
|
| 771 |
+
## CRITICAL SPELLING RULES - MUST FOLLOW:
|
| 772 |
+
1. ✅ "مجھے" ❌ "مجہے"
|
| 773 |
+
2. ✅ "پروگرام" ❌ "پروگرہوں"
|
| 774 |
+
3. ✅ "کینسر" ❌ "کہےنسر"
|
| 775 |
+
4. ✅ "ڈاکٹر" ❌ "ڈڈاکٹر"
|
| 776 |
+
5. ✅ "ہے" ❌ "ہےہ"
|
| 777 |
+
6. ✅ "میں" ❌ "مہےں"
|
| 778 |
+
7. ✅ "کے لیے" ❌ "کا ے لہےے"
|
| 779 |
+
8. ✅ "جس سے" ❌ "جسے سے"
|
| 780 |
+
|
| 781 |
+
## RESPONSE REQUIREMENTS - URDU:
|
| 782 |
+
1. **LANGUAGE:** صرف اردو میں جواب دیں، درست ہجے اور قواعد کا استعمال کریں
|
| 783 |
+
2. **EMOTIONAL TONE:** ہمدردانہ، گرمجوش، اور امید بخش انداز اپنائیں
|
| 784 |
+
3. **CONTENT:** اگر معلومات درکار ہوں تو واضح، درست معلومات دیں
|
| 785 |
+
4. **SUPPORT:** جذباتی مدد قدرتی طور پر پیش کریں، الگ سے ذکر نہ کریں
|
| 786 |
+
5. **LENGTH:** 4-6 جملے، مختصر مگر جامع
|
| 787 |
+
6. **SPELLING:** درست اردو ہجے استعمال کریں، غلط ہجے سے پرہیز کریں
|
| 788 |
+
7. **COMPLETENESS:** مکمل جملے لکھیں، ادھورے جملے نہ چھوڑیں
|
| 789 |
+
|
| 790 |
+
## آپ کا گرمجوش، درست اردو میں اور مکمل جواب:
|
| 791 |
+
"""
|
| 792 |
+
else:
|
| 793 |
+
prompt = f"""
|
| 794 |
+
# WELL BEING AGENT - BREAST CANCER SUPPORT
|
| 795 |
+
|
| 796 |
+
## PATIENT'S QUERY:
|
| 797 |
+
"{user_query}"
|
| 798 |
+
|
| 799 |
+
## EMOTIONAL ANALYSIS:
|
| 800 |
+
- Needs Emotional Support: {'YES' if needs_analysis['needs_emotional_support'] else 'NO'}
|
| 801 |
+
- Needs Information: {'YES' if needs_analysis['needs_information'] else 'NO'}
|
| 802 |
+
|
| 803 |
+
## CONTEXT (USE IF RELEVANT):
|
| 804 |
+
{context_text if context_text else "General breast cancer knowledge"}
|
| 805 |
+
|
| 806 |
+
## RESPONSE REQUIREMENTS:
|
| 807 |
+
|
| 808 |
+
1. **TONE:** Warm, compassionate, and hopeful
|
| 809 |
+
2. **CONTENT:** Provide accurate information if needed
|
| 810 |
+
3. **SUPPORT:** Integrate emotional support naturally without explicitly stating it
|
| 811 |
+
4. **LENGTH:** 4-6 sentences, concise but comprehensive
|
| 812 |
+
5. **FOCUS:** Be caring and present with the patient
|
| 813 |
+
6. **COMPLETENESS:** Write complete sentences, no incomplete thoughts
|
| 814 |
+
|
| 815 |
+
## YOUR COMPASSIONATE RESPONSE:
|
| 816 |
+
"""
|
| 817 |
+
|
| 818 |
+
return prompt.strip()
|
| 819 |
+
|
| 820 |
+
def build_urdu_prompt(self, user_query: str, context_chunks: List[Any]) -> str:
|
| 821 |
+
"""Build detailed prompt for Urdu responses with strong language enforcement"""
|
| 822 |
+
context_text = ""
|
| 823 |
+
if context_chunks:
|
| 824 |
+
context_parts = []
|
| 825 |
+
for i, chunk in enumerate(context_chunks[:3]):
|
| 826 |
+
chunk_text = chunk.text if hasattr(chunk, 'text') else str(chunk)
|
| 827 |
+
source_topic = chunk.metadata.get('topic', 'General Information') if hasattr(chunk, 'metadata') else 'General Information'
|
| 828 |
+
context_parts.append(f"SOURCE {i+1} - {source_topic}:\n{chunk_text}")
|
| 829 |
+
context_text = "\n\n".join(context_parts)
|
| 830 |
+
|
| 831 |
+
urdu_prompt = f"""
|
| 832 |
+
# WELL BEING AGENT - BREAST CANCER SUPPORT
|
| 833 |
+
# CRITICAL: RESPOND ONLY IN URDU LANGUAGE WITH PERFECT SPELLING
|
| 834 |
+
# ABSOLUTELY NO HINDI, ARABIC, OR ENGLISH - PURE URDU ONLY
|
| 835 |
+
|
| 836 |
+
## YOUR ROLE IN URDU:
|
| 837 |
+
آپ بریسٹ کینسر کی سپیشلائزڈ ویل بینگ ایجنٹ ہیں۔ آپ مریضوں کو نہ صرف طبی معلومات بلکہ قدرتی طور پر جذباتی مدد اور ہمت بھی فراہم کرتی ہیں۔
|
| 838 |
+
|
| 839 |
+
## AVAILABLE CONTEXT:
|
| 840 |
+
{context_text if context_text else "General breast cancer knowledge"}
|
| 841 |
+
|
| 842 |
+
## USER'S QUESTION (IN URDU):
|
| 843 |
+
"{user_query}"
|
| 844 |
+
|
| 845 |
+
## CRITICAL SPELLING RULES - MUST FOLLOW:
|
| 846 |
+
1. ✅ "مجھے" ❌ "مجہے"
|
| 847 |
+
2. ✅ "پروگرام" ❌ "پروگرہوں"
|
| 848 |
+
3. ✅ "کینسر" ❌ "کہےنسر"
|
| 849 |
+
4. ✅ "ڈاکٹر" ❌ "ڈڈاکٹر"
|
| 850 |
+
5. ✅ "ہے" ❌ "ہےہ"
|
| 851 |
+
6. ✅ "میں" ❌ "مہےں"
|
| 852 |
+
7. ✅ "کے لیے" ❌ "کا ے لہےے"
|
| 853 |
+
8. ✅ "جس سے" ❌ "جسے سے"
|
| 854 |
+
|
| 855 |
+
## RESPONSE REQUIREMENTS - URDU:
|
| 856 |
+
1. **LANGUAGE ENFORCEMENT:** صرف اور صرف اردو میں جواب دیں
|
| 857 |
+
2. **SPELLING ACCURACY:** درست اردو ہجے استعمال کریں، عام غلطیوں سے پرہیز کریں
|
| 858 |
+
3. **EMOTIONAL INTEGRATION:** جذباتی مدد کو قدرتی انداز میں پیش کریں
|
| 859 |
+
4. **COMPASSIONATE TONE:** گرمجوش، ہمدردانہ، اور امید بخش انداز
|
| 860 |
+
5. **INFORMATION ACCURACY:** سیاق و سباق کے مطابق درست معلومات دیں
|
| 861 |
+
6. **COMPLETE SENTENCES:** مکمل جملے لکھیں، ادھورے جملے نہ چھوڑیں
|
| 862 |
+
|
| 863 |
+
## EXAMPLES OF CORRECT URDU:
|
| 864 |
+
- ✅ "بریسٹ کینسر کے بارے میں معلومات حاصل کرنا ایک اہم قدم ہے۔"
|
| 865 |
+
- ✅ "میں آپ کو درست معلومات فراہم کرنے کی کوشش کروں گی۔"
|
| 866 |
+
- ✅ "آپ کے سوال کا جواب دینے میں مجھے خوشی ہو رہی ہے۔"
|
| 867 |
+
|
| 868 |
+
## آپ کا درست ہجے، مکمل جملوں اور ہمدردانہ انداز میں جواب:
|
| 869 |
+
"""
|
| 870 |
+
return urdu_prompt.strip()
|
| 871 |
+
|
| 872 |
+
def build_enhanced_prompt(self, user_query: str, context_chunks: List[Any]) -> str:
|
| 873 |
+
"""Build prompt for English responses with emotional intelligence"""
|
| 874 |
+
context_text = ""
|
| 875 |
+
if context_chunks:
|
| 876 |
+
context_parts = []
|
| 877 |
+
for i, chunk in enumerate(context_chunks[:3]):
|
| 878 |
+
chunk_text = chunk.text if hasattr(chunk, 'text') else str(chunk)
|
| 879 |
+
source_topic = chunk.metadata.get('topic', 'General Information') if hasattr(chunk, 'metadata') else 'General Information'
|
| 880 |
+
context_parts.append(f"SOURCE {i+1} - {source_topic}:\n{chunk_text}")
|
| 881 |
+
context_text = "\n\n".join(context_parts)
|
| 882 |
+
|
| 883 |
+
# Analyze emotional needs
|
| 884 |
+
needs_analysis = self._detect_emotional_needs(user_query, "english")
|
| 885 |
+
|
| 886 |
+
prompt = f"""
|
| 887 |
+
# WELL BEING AGENT - BREAST CANCER SUPPORT
|
| 888 |
+
|
| 889 |
+
## YOUR ROLE
|
| 890 |
+
You are a compassionate Well Being Agent specializing in breast cancer support. You provide supportive information, emotional comfort, and evidence-based guidance.
|
| 891 |
+
|
| 892 |
+
## EMOTIONAL ANALYSIS:
|
| 893 |
+
- Patient Needs Emotional Support: {'YES' if needs_analysis['needs_emotional_support'] else 'NO'}
|
| 894 |
+
- Patient Needs Information: {'YES' if needs_analysis['needs_information'] else 'NO'}
|
| 895 |
+
|
| 896 |
+
## RESPONSE GUIDELINES
|
| 897 |
+
- **Tone**: Warm, supportive, compassionate, and hopeful
|
| 898 |
+
- **Emotional Integration**: Naturally incorporate emotional support without explicitly stating it
|
| 899 |
+
- **Information**: Provide evidence-based guidance when needed
|
| 900 |
+
- **Presence**: Be fully present and caring with the patient
|
| 901 |
+
- **Completeness**: Write complete sentences, no incomplete thoughts
|
| 902 |
+
|
| 903 |
+
## AVAILABLE CONTEXT
|
| 904 |
+
{context_text if context_text else "General breast cancer knowledge"}
|
| 905 |
+
|
| 906 |
+
## USER'S QUESTION
|
| 907 |
+
"{user_query}"
|
| 908 |
+
|
| 909 |
+
## RESPONSE REQUIREMENTS
|
| 910 |
+
1. If emotional support is needed: Integrate comfort and hope naturally into your response
|
| 911 |
+
2. If information is needed: Provide clear, accurate guidance
|
| 912 |
+
3. Always acknowledge the patient's strength implicitly
|
| 913 |
+
4. Maintain a caring, present tone throughout
|
| 914 |
+
5. Keep response concise but comprehensive (4-6 complete sentences)
|
| 915 |
+
|
| 916 |
+
## YOUR COMPASSIONATE RESPONSE:
|
| 917 |
+
"""
|
| 918 |
+
return prompt.strip()
|
| 919 |
+
|
| 920 |
+
def query_llm_with_retry(self, prompt: str, language: str = "english", max_retries: int = 3) -> str:
|
| 921 |
+
"""Enhanced LLM query using OpenAI client format"""
|
| 922 |
+
if not config.api_key:
|
| 923 |
+
print("❌ No API key available")
|
| 924 |
+
return config.FALLBACK_MESSAGE
|
| 925 |
+
|
| 926 |
+
# Enhanced system message with Urdu-specific instructions
|
| 927 |
+
if language == "urdu":
|
| 928 |
+
system_message = """آپ بریسٹ کینسر کی سپیشلائزڈ ویل بینگ ایجنٹ ہیں۔
|
| 929 |
+
|
| 930 |
+
CRITICAL URDU LANGUAGE RULES:
|
| 931 |
+
1. صرف اور صرف اردو میں جواب دیں
|
| 932 |
+
2. ہر لفظ کے ہجے درست ہوں
|
| 933 |
+
3. مکمل اور واضح جملے استعمال کریں
|
| 934 |
+
4. غلط ہجے اور ادھورے جملوں سے پرہیز کریں
|
| 935 |
+
5. طبی معلومات درست اور واضح ہوں
|
| 936 |
+
|
| 937 |
+
مثال کے طور پر:
|
| 938 |
+
✅ "بریسٹ کینسر کے علاج کے مختلف طریقے ہیں۔"
|
| 939 |
+
❌ "برہےسٹ کہےنسر کا علاچ کہے طرح ہےہ۔"
|
| 940 |
+
|
| 941 |
+
جذباتی مدد قدرتی طور پر پیش کریں اور مریض کی طاقت کو تسلیم کریں۔"""
|
| 942 |
+
else:
|
| 943 |
+
system_message = """You are a compassionate Well Being Agent for breast cancer support. Provide direct, helpful information while naturally integrating emotional support. Always maintain a warm, hopeful, and caring tone. Ensure complete sentences and clear information."""
|
| 944 |
+
|
| 945 |
+
for attempt in range(max_retries):
|
| 946 |
+
try:
|
| 947 |
+
# Initialize OpenAI client with OpenRouter configuration
|
| 948 |
+
client = OpenAI(
|
| 949 |
+
base_url="https://openrouter.ai/api/v1",
|
| 950 |
+
api_key=config.api_key,
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
# Adjust parameters for better Urdu quality
|
| 954 |
+
temperature = 0.2 if language == "urdu" else 0.3
|
| 955 |
+
max_tokens = 500 if language == "urdu" else config.MAX_TOKENS
|
| 956 |
+
|
| 957 |
+
print(f"🔄 Sending request to {config.MODEL_PROVIDER} (attempt {attempt + 1})")
|
| 958 |
+
|
| 959 |
+
completion = client.chat.completions.create(
|
| 960 |
+
extra_headers={
|
| 961 |
+
"HTTP-Referer": "https://huggingface.co",
|
| 962 |
+
"X-Title": "Well Being Agent",
|
| 963 |
+
},
|
| 964 |
+
extra_body={},
|
| 965 |
+
model=config.MODEL_ID,
|
| 966 |
+
messages=[
|
| 967 |
+
{
|
| 968 |
+
"role": "system",
|
| 969 |
+
"content": system_message
|
| 970 |
+
},
|
| 971 |
+
{
|
| 972 |
+
"role": "user",
|
| 973 |
+
"content": prompt
|
| 974 |
+
}
|
| 975 |
+
],
|
| 976 |
+
temperature=temperature,
|
| 977 |
+
max_tokens=max_tokens,
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
+
response_text = completion.choices[0].message.content
|
| 981 |
+
print("✅ LLM response received")
|
| 982 |
+
|
| 983 |
+
# For Urdu, do immediate quality check
|
| 984 |
+
if language == "urdu":
|
| 985 |
+
if self._is_urdu_response_corrupted(response_text):
|
| 986 |
+
print("⚠️ Urdu response appears corrupted, applying enhanced cleaning")
|
| 987 |
+
|
| 988 |
+
return response_text
|
| 989 |
+
|
| 990 |
+
except Exception as e:
|
| 991 |
+
print(f"❌ Request failed: {e}")
|
| 992 |
+
if "429" in str(e):
|
| 993 |
+
wait_time = 2 ** attempt
|
| 994 |
+
print(f"⏳ Rate limited. Waiting {wait_time} seconds...")
|
| 995 |
+
time.sleep(wait_time)
|
| 996 |
+
continue
|
| 997 |
+
elif "401" in str(e) or "402" in str(e):
|
| 998 |
+
print(f"❌ API key issue")
|
| 999 |
+
if config.rotate_to_next_key():
|
| 1000 |
+
continue
|
| 1001 |
+
else:
|
| 1002 |
+
return config.FALLBACK_MESSAGE
|
| 1003 |
+
|
| 1004 |
+
if attempt == max_retries - 1:
|
| 1005 |
+
if config.rotate_to_next_key():
|
| 1006 |
+
return self.query_llm_with_retry(prompt, language, max_retries)
|
| 1007 |
+
return config.FALLBACK_MESSAGE
|
| 1008 |
+
time.sleep(1)
|
| 1009 |
+
|
| 1010 |
+
return config.FALLBACK_MESSAGE
|
| 1011 |
+
|
| 1012 |
+
def _is_urdu_response_corrupted(self, text: str) -> bool:
|
| 1013 |
+
"""Check if Urdu response has common corruption patterns"""
|
| 1014 |
+
corruption_indicators = [
|
| 1015 |
+
'ہےہ', 'مہےں', 'کہے', 'پروگرہوں', 'ڈڈاکٹر', 'کا ے لہےے', 'جسے سے'
|
| 1016 |
+
]
|
| 1017 |
+
|
| 1018 |
+
for indicator in corruption_indicators:
|
| 1019 |
+
if indicator in text:
|
| 1020 |
+
return True
|
| 1021 |
+
|
| 1022 |
+
# Check for excessive character repetition
|
| 1023 |
+
import re
|
| 1024 |
+
if re.search(r'(.)\1\1', text): # Three repeated characters
|
| 1025 |
+
return True
|
| 1026 |
+
|
| 1027 |
+
return False
|
| 1028 |
+
|
| 1029 |
+
def _verify_language_compliance(self, text: str, expected_language: str) -> str:
|
| 1030 |
+
"""Verify and correct language compliance"""
|
| 1031 |
+
if expected_language == "urdu":
|
| 1032 |
+
# Check for common incorrect language patterns
|
| 1033 |
+
hindi_pattern = re.compile(r'[\u0900-\u097F]+') # Hindi characters
|
| 1034 |
+
arabic_pattern = re.compile(r'[\uFE70-\uFEFF]+') # Arabic specific characters
|
| 1035 |
+
|
| 1036 |
+
if hindi_pattern.search(text):
|
| 1037 |
+
print("⚠️ Hindi detected in Urdu response, applying correction...")
|
| 1038 |
+
# Add Urdu language reminder
|
| 1039 |
+
return text + "\n\nبراہ کرم صرف اردو میں جواب دیں۔"
|
| 1040 |
+
|
| 1041 |
+
if arabic_pattern.search(text):
|
| 1042 |
+
print("⚠️ Arabic detected in Urdu response, applying correction...")
|
| 1043 |
+
# Add Urdu language reminder
|
| 1044 |
+
return text + "\n\nبراہ کرم صرف اردو میں جواب دیں۔"
|
| 1045 |
+
|
| 1046 |
+
return text
|
| 1047 |
+
|
| 1048 |
+
def format_final_response(self, llm_answer: str, language: str = "english") -> str:
|
| 1049 |
+
cleaned_answer = llm_answer.strip()
|
| 1050 |
+
|
| 1051 |
+
# Enhanced Urdu text cleaning
|
| 1052 |
+
if language == 'urdu':
|
| 1053 |
+
print("🧹 Applying advanced Urdu text cleaning...")
|
| 1054 |
+
cleaned_answer = self._clean_urdu_text(cleaned_answer)
|
| 1055 |
+
|
| 1056 |
+
# Verify language compliance
|
| 1057 |
+
cleaned_answer = self._verify_language_compliance(cleaned_answer, language)
|
| 1058 |
+
|
| 1059 |
+
if language == 'urdu':
|
| 1060 |
+
gentle_reminder = "\n\nاپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکھیں۔"
|
| 1061 |
+
else:
|
| 1062 |
+
gentle_reminder = "\n\nRemember to discuss any concerns with your healthcare team."
|
| 1063 |
+
|
| 1064 |
+
if gentle_reminder not in cleaned_answer:
|
| 1065 |
+
cleaned_answer += gentle_reminder
|
| 1066 |
+
|
| 1067 |
+
return cleaned_answer.strip()
|
| 1068 |
+
|
| 1069 |
+
def get_enhanced_answer(self, user_query: str, language: str = None, response_type: str = "text") -> str:
|
| 1070 |
+
print(f"🔍 Processing query: '{user_query}' (Type: {response_type})")
|
| 1071 |
+
|
| 1072 |
+
if language is None:
|
| 1073 |
+
language = self.detect_language(user_query)
|
| 1074 |
+
print(f"🌐 Detected language: {language}")
|
| 1075 |
+
|
| 1076 |
+
# Special handling for problematic Urdu queries
|
| 1077 |
+
if language == "urdu":
|
| 1078 |
+
problematic_patterns = ['اوج ایک انسر', 'اصلاح ملکم', 'نعم']
|
| 1079 |
+
if any(pattern in user_query for pattern in problematic_patterns):
|
| 1080 |
+
print("⚠️ Detected problematic query pattern, applying enhanced Urdu handling")
|
| 1081 |
+
|
| 1082 |
+
chunks = self.retrieve_relevant_chunks(user_query, language)
|
| 1083 |
+
|
| 1084 |
+
cache_key = response_cache.get_cache_key(user_query, chunks)
|
| 1085 |
+
cached_response = response_cache.get(cache_key)
|
| 1086 |
+
|
| 1087 |
+
if cached_response:
|
| 1088 |
+
print("✅ Using cached response")
|
| 1089 |
+
final_answer = cached_response
|
| 1090 |
+
else:
|
| 1091 |
+
# Enhanced prompt selection with quality focus
|
| 1092 |
+
query_lower = user_query.lower()
|
| 1093 |
+
wants_details = any(phrase in query_lower for phrase in [
|
| 1094 |
+
"give details", "more detail", "explain more", "tell me more",
|
| 1095 |
+
"elaborate", "in detail", "detailed", "comprehensive"
|
| 1096 |
+
])
|
| 1097 |
+
|
| 1098 |
+
if language == 'urdu':
|
| 1099 |
+
if wants_details:
|
| 1100 |
+
prompt = self.build_urdu_prompt(user_query, chunks)
|
| 1101 |
+
else:
|
| 1102 |
+
prompt = self.build_concise_prompt(user_query, chunks, language)
|
| 1103 |
+
else:
|
| 1104 |
+
if wants_details:
|
| 1105 |
+
prompt = self.build_enhanced_prompt(user_query, chunks)
|
| 1106 |
+
else:
|
| 1107 |
+
prompt = self.build_concise_prompt(user_query, chunks, language)
|
| 1108 |
+
|
| 1109 |
+
llm_answer = self.query_llm_with_retry(prompt, language)
|
| 1110 |
+
|
| 1111 |
+
# Enhanced cleaning and validation for Urdu
|
| 1112 |
+
if language == 'urdu':
|
| 1113 |
+
original_length = len(llm_answer.strip().split())
|
| 1114 |
+
llm_answer = self.format_final_response(llm_answer, language)
|
| 1115 |
+
cleaned_length = len(llm_answer.strip().split())
|
| 1116 |
+
|
| 1117 |
+
if cleaned_length < 5: # Too short
|
| 1118 |
+
print("⚠️ Urdu response too short, may be incomplete")
|
| 1119 |
+
elif cleaned_length < original_length * 0.7: # Significant reduction
|
| 1120 |
+
print("⚠️ Significant text reduction during cleaning")
|
| 1121 |
+
|
| 1122 |
+
final_answer = self.format_final_response(llm_answer, language)
|
| 1123 |
+
|
| 1124 |
+
# Always add emotional support naturally
|
| 1125 |
+
final_answer = self._add_emotional_support(final_answer, user_query, language)
|
| 1126 |
+
|
| 1127 |
+
response_cache.set(cache_key, final_answer)
|
| 1128 |
+
print("💾 Response cached for future use")
|
| 1129 |
+
|
| 1130 |
+
# Log conversation to JSON
|
| 1131 |
+
conversation_logger.log_conversation(
|
| 1132 |
+
user_input=user_query,
|
| 1133 |
+
llm_response=final_answer,
|
| 1134 |
+
language=language,
|
| 1135 |
+
response_type=response_type
|
| 1136 |
+
)
|
| 1137 |
+
|
| 1138 |
+
self.conversation_history.append({
|
| 1139 |
+
"query": user_query,
|
| 1140 |
+
"answer": final_answer,
|
| 1141 |
+
"language": language,
|
| 1142 |
+
"response_type": response_type,
|
| 1143 |
+
"timestamp": time.time()
|
| 1144 |
+
})
|
| 1145 |
+
|
| 1146 |
+
return final_answer
|
| 1147 |
+
|
| 1148 |
+
# === Pre-load Index at Module Level ===
|
| 1149 |
+
print("🚀 Starting Well Being Agent with optimized loading...")
|
| 1150 |
+
_start_time = time.time()
|
| 1151 |
+
|
| 1152 |
+
print("🔄 Loading vector index...")
|
| 1153 |
+
index, retriever = load_index_fast()
|
| 1154 |
+
|
| 1155 |
+
_load_time = time.time() - _start_time
|
| 1156 |
+
print(f"✅ System ready in {_load_time:.2f} seconds")
|
| 1157 |
+
|
| 1158 |
+
# Create global RAG system instance
|
| 1159 |
+
rag_system = BreastCancerRAGSystem(index, retriever)
|
| 1160 |
+
|
| 1161 |
+
# === Interactive Chat Mode ===
|
| 1162 |
+
def interactive_chat():
|
| 1163 |
+
print("💬 Well Being Agent - Breast Cancer Support")
|
| 1164 |
+
print("=" * 50)
|
| 1165 |
+
print("Type 'quit' to exit, 'topics' to see available topics, 'cache' for cache stats")
|
| 1166 |
+
print("=" * 50)
|
| 1167 |
+
|
| 1168 |
+
global rag_system
|
| 1169 |
+
|
| 1170 |
+
while True:
|
| 1171 |
+
user_input = input("\n❓ Your question: ").strip()
|
| 1172 |
+
if user_input.lower() in ['quit', 'exit', 'q']:
|
| 1173 |
+
break
|
| 1174 |
+
elif user_input.lower() == 'topics':
|
| 1175 |
+
print("\n📚 Available topics: Fertility, Treatment, Symptoms, Diagnosis, etc.")
|
| 1176 |
+
continue
|
| 1177 |
+
elif user_input.lower() == 'cache':
|
| 1178 |
+
print(f"\n📊 Cache stats: {len(response_cache.cache)} cached responses")
|
| 1179 |
+
continue
|
| 1180 |
+
elif not user_input:
|
| 1181 |
+
continue
|
| 1182 |
+
|
| 1183 |
+
print("🤔 Thinking...")
|
| 1184 |
+
start_time = time.time()
|
| 1185 |
+
answer = rag_system.get_enhanced_answer(user_input)
|
| 1186 |
+
response_time = time.time() - start_time
|
| 1187 |
+
print(f"\n💡 {answer}")
|
| 1188 |
+
print(f"⏱️ Response time: {response_time:.2f} seconds")
|
| 1189 |
+
|
| 1190 |
+
# === Main Function ===
|
| 1191 |
+
def main():
|
| 1192 |
+
print("🏥 Well Being Agent - Breast Cancer Support System")
|
| 1193 |
+
print("=" * 50)
|
| 1194 |
+
print(f"📋 Current Configuration:")
|
| 1195 |
+
print(f" Model: {config.MODEL_ID}")
|
| 1196 |
+
print(f" Provider: {config.MODEL_PROVIDER}")
|
| 1197 |
+
print(f" Index: {config.INDEX_PATH}")
|
| 1198 |
+
print(f" Cache: {len(response_cache.cache)} responses loaded")
|
| 1199 |
+
print("=" * 50)
|
| 1200 |
+
|
| 1201 |
+
if not config.api_keys:
|
| 1202 |
+
print("❌ API keys not configured.")
|
| 1203 |
+
if IS_HUGGING_FACE:
|
| 1204 |
+
print("💡 Add API keys in Space Settings → Repository secrets")
|
| 1205 |
+
return
|
| 1206 |
+
|
| 1207 |
+
interactive_chat()
|
| 1208 |
+
|
| 1209 |
+
if __name__ == "__main__":
|
| 1210 |
+
main()
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile for deploying the Hugging Face application
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies including audio libraries
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
gcc \
|
| 9 |
+
g++ \
|
| 10 |
+
git \
|
| 11 |
+
ffmpeg \
|
| 12 |
+
portaudio19-dev \
|
| 13 |
+
curl \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 15 |
+
&& apt-get clean
|
| 16 |
+
|
| 17 |
+
# Copy requirements first for better caching
|
| 18 |
+
COPY requirements.txt .
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copy application files
|
| 22 |
+
COPY . .
|
| 23 |
+
|
| 24 |
+
# Create necessary directories
|
| 25 |
+
RUN mkdir -p static/audio config cache DataSet
|
| 26 |
+
|
| 27 |
+
# Expose port (Hugging Face uses 7860)
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Health check - use the correct port
|
| 31 |
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 32 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 33 |
+
|
| 34 |
+
# Start command for Hugging Face - FIXED PORT
|
| 35 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
Index.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Index.py - Multilingual version (English + Urdu) for creating embeddings and vector index
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
# === Centralized Configuration System ===
|
| 8 |
+
class Config:
|
| 9 |
+
"""Centralized configuration - loads from config.json"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
# Load settings from config.json
|
| 13 |
+
self.settings = self._load_config_file()
|
| 14 |
+
|
| 15 |
+
# Apply settings
|
| 16 |
+
self.INDEX_PATH = self.settings["index_path"]
|
| 17 |
+
self.DATASET_PATH = self._find_dataset_path()
|
| 18 |
+
self._validate_config()
|
| 19 |
+
|
| 20 |
+
def _load_config_file(self):
|
| 21 |
+
"""Load configuration from config/config.json file"""
|
| 22 |
+
config_file = os.path.join("config", "config.json")
|
| 23 |
+
default_config = {
|
| 24 |
+
"model_provider": "openrouter",
|
| 25 |
+
"model_id": "meta-llama/llama-3.3-70b-instruct:free",
|
| 26 |
+
"api_keys_folder": "config",
|
| 27 |
+
"index_path": "cancer_index_store",
|
| 28 |
+
"dataset_path": "DataSet/breast_cancer.json",
|
| 29 |
+
"similarity_top_k": 5,
|
| 30 |
+
"temperature": 0.2,
|
| 31 |
+
"max_tokens": 350,
|
| 32 |
+
"combine_sources": True,
|
| 33 |
+
"fallback_message": "Sorry, I don't know the answer.",
|
| 34 |
+
"strict_breast_cancer_only": True
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
if os.path.exists(config_file):
|
| 39 |
+
with open(config_file, 'r', encoding='utf-8') as f:
|
| 40 |
+
loaded_config = json.load(f)
|
| 41 |
+
# Merge with defaults for missing keys
|
| 42 |
+
merged_config = {**default_config, **loaded_config}
|
| 43 |
+
logging.info("✅ Configuration loaded from config/config.json")
|
| 44 |
+
return merged_config
|
| 45 |
+
else:
|
| 46 |
+
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
| 47 |
+
with open(config_file, 'w', encoding='utf-8') as f:
|
| 48 |
+
json.dump(default_config, f, indent=4)
|
| 49 |
+
logging.info("📁 Created default config/config.json file")
|
| 50 |
+
return default_config
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logging.error(f"❌ Error loading config/config.json: {e}")
|
| 53 |
+
logging.info("🔄 Using default configuration")
|
| 54 |
+
return default_config
|
| 55 |
+
|
| 56 |
+
def _find_dataset_path(self):
|
| 57 |
+
"""Find the correct dataset path"""
|
| 58 |
+
original_path = self.settings["dataset_path"]
|
| 59 |
+
possible_paths = [
|
| 60 |
+
original_path,
|
| 61 |
+
f"DataSet/{original_path}",
|
| 62 |
+
f"data/{original_path}",
|
| 63 |
+
"DataSet/breast_cancer.json",
|
| 64 |
+
"breast_cancer.json",
|
| 65 |
+
"../DataSet/breast_cancer.json"
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
for path in possible_paths:
|
| 69 |
+
if os.path.exists(path):
|
| 70 |
+
if path != original_path:
|
| 71 |
+
logging.info(f"🔄 Using dataset at: {path}")
|
| 72 |
+
else:
|
| 73 |
+
logging.info(f"✅ Dataset found at: {path}")
|
| 74 |
+
return path
|
| 75 |
+
|
| 76 |
+
logging.error(f"❌ Dataset not found in any location")
|
| 77 |
+
return original_path # Return original even if not found for error handling
|
| 78 |
+
|
| 79 |
+
def _validate_config(self):
|
| 80 |
+
"""Validate configuration"""
|
| 81 |
+
if not os.path.exists(self.DATASET_PATH):
|
| 82 |
+
logging.error(f"❌ Dataset file not found: {self.DATASET_PATH}")
|
| 83 |
+
else:
|
| 84 |
+
logging.info(f"✅ Dataset found: {self.DATASET_PATH}")
|
| 85 |
+
logging.info(f"✅ Index will be stored at: {self.INDEX_PATH}")
|
| 86 |
+
|
| 87 |
+
# Initialize configuration
|
| 88 |
+
config = Config()
|
| 89 |
+
|
| 90 |
+
# === Setup Logging ===
|
| 91 |
+
logging.basicConfig(
|
| 92 |
+
level=logging.INFO,
|
| 93 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 94 |
+
handlers=[logging.StreamHandler()]
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def create_vector_index():
|
| 98 |
+
"""
|
| 99 |
+
Creates embeddings and builds vector index from dataset
|
| 100 |
+
Supports both English and Urdu text.
|
| 101 |
+
"""
|
| 102 |
+
try:
|
| 103 |
+
from llama_index.core import VectorStoreIndex, Document, StorageContext
|
| 104 |
+
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 105 |
+
from llama_index.core.node_parser import SimpleNodeParser
|
| 106 |
+
|
| 107 |
+
print("🚀 Starting Multilingual Vector Index Creation...")
|
| 108 |
+
print("=" * 60)
|
| 109 |
+
|
| 110 |
+
# Check dataset
|
| 111 |
+
if not os.path.exists(config.DATASET_PATH):
|
| 112 |
+
print(f"❌ Dataset not found: {config.DATASET_PATH}")
|
| 113 |
+
return False
|
| 114 |
+
|
| 115 |
+
# Load dataset
|
| 116 |
+
print(f"📖 Loading dataset from: {config.DATASET_PATH}")
|
| 117 |
+
with open(config.DATASET_PATH, 'r', encoding='utf-8') as f:
|
| 118 |
+
dataset = json.load(f)
|
| 119 |
+
print(f"✅ Loaded dataset with {len(dataset)} entries")
|
| 120 |
+
|
| 121 |
+
# Normalize dataset for multilingual consistency
|
| 122 |
+
documents = []
|
| 123 |
+
for item in dataset:
|
| 124 |
+
if isinstance(item, dict):
|
| 125 |
+
text = item.get('content') or item.get('text') or item.get('answer') or str(item)
|
| 126 |
+
if not text or len(text.strip()) < 10:
|
| 127 |
+
continue # skip empty
|
| 128 |
+
metadata = {k: v for k, v in item.items() if k not in ['content', 'text', 'answer']}
|
| 129 |
+
documents.append(Document(text=text.strip(), metadata=metadata))
|
| 130 |
+
else:
|
| 131 |
+
documents.append(Document(text=str(item)))
|
| 132 |
+
|
| 133 |
+
print(f"✅ Created {len(documents)} documents for embedding")
|
| 134 |
+
|
| 135 |
+
# === Multilingual embedding model ===
|
| 136 |
+
# Supports 50+ languages including Urdu + English
|
| 137 |
+
multilingual_model = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
| 138 |
+
print(f"🔧 Loading embedding model: {multilingual_model}")
|
| 139 |
+
embed_model = HuggingFaceEmbedding(model_name=multilingual_model)
|
| 140 |
+
|
| 141 |
+
# Create node parser
|
| 142 |
+
node_parser = SimpleNodeParser.from_defaults(chunk_size=512, chunk_overlap=50)
|
| 143 |
+
|
| 144 |
+
# Parse documents
|
| 145 |
+
print("🔨 Parsing documents into nodes...")
|
| 146 |
+
nodes = node_parser.get_nodes_from_documents(documents)
|
| 147 |
+
print(f"✅ Created {len(nodes)} nodes")
|
| 148 |
+
|
| 149 |
+
# Build index
|
| 150 |
+
print("🏗️ Building multilingual vector index...")
|
| 151 |
+
index = VectorStoreIndex(nodes=nodes, embed_model=embed_model, show_progress=True)
|
| 152 |
+
|
| 153 |
+
# Persist
|
| 154 |
+
os.makedirs(config.INDEX_PATH, exist_ok=True)
|
| 155 |
+
print(f"💾 Saving index to: {config.INDEX_PATH}")
|
| 156 |
+
index.storage_context.persist(persist_dir=config.INDEX_PATH)
|
| 157 |
+
|
| 158 |
+
print("✅ Multilingual vector index created successfully!")
|
| 159 |
+
print(f"📁 Index location: {config.INDEX_PATH}")
|
| 160 |
+
print(f"📊 Total nodes embedded: {len(nodes)}")
|
| 161 |
+
|
| 162 |
+
# Test retrieval in both languages
|
| 163 |
+
retriever = index.as_retriever(similarity_top_k=2)
|
| 164 |
+
print("🔍 Testing bilingual retrieval:")
|
| 165 |
+
en_test = retriever.retrieve("What are the symptoms of breast cancer?")
|
| 166 |
+
ur_test = retriever.retrieve("بریسٹ کینسر کی علامات کیا ہیں؟")
|
| 167 |
+
print(f"✅ English test retrieved {len(en_test)} results")
|
| 168 |
+
print(f"✅ Urdu test retrieved {len(ur_test)} results")
|
| 169 |
+
|
| 170 |
+
print("\n🎉 Multilingual index ready for RAG pipeline!")
|
| 171 |
+
return True
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"❌ Failed to create multilingual vector index: {e}")
|
| 175 |
+
import traceback; traceback.print_exc()
|
| 176 |
+
return False
|
| 177 |
+
|
| 178 |
+
def check_index_exists():
|
| 179 |
+
if os.path.exists(config.INDEX_PATH):
|
| 180 |
+
print(f"✅ Index already exists at: {config.INDEX_PATH}")
|
| 181 |
+
return True
|
| 182 |
+
else:
|
| 183 |
+
print(f"❌ Index not found at: {config.INDEX_PATH}")
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
def main():
|
| 187 |
+
print("🏥 Well Being Agent - Multilingual Index Creator")
|
| 188 |
+
print("=" * 60)
|
| 189 |
+
|
| 190 |
+
if check_index_exists():
|
| 191 |
+
response = input("Index already exists. Recreate? (y/n): ").strip().lower()
|
| 192 |
+
if response != 'y':
|
| 193 |
+
print("Operation cancelled.")
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
success = create_vector_index()
|
| 197 |
+
if success:
|
| 198 |
+
print("\n🎯 Next steps:")
|
| 199 |
+
print("1️⃣ Run Agent.py for RAG operations")
|
| 200 |
+
print("2️⃣ Run app.py for web interface")
|
| 201 |
+
else:
|
| 202 |
+
print("\n💥 Index creation failed!")
|
| 203 |
+
|
| 204 |
+
if __name__ == "__main__":
|
| 205 |
+
main()
|
app.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py - Enhanced with voice query support (Text responses only)
|
| 2 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
import asyncio
|
| 11 |
+
import time
|
| 12 |
+
import uuid
|
| 13 |
+
from typing import Optional
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
# Set up logging
|
| 17 |
+
logging.basicConfig(level=logging.INFO)
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
# Add current directory to path to fix import issues
|
| 21 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
+
|
| 23 |
+
# Global RAG system instance
|
| 24 |
+
rag_system = None
|
| 25 |
+
|
| 26 |
+
@asynccontextmanager
|
| 27 |
+
async def lifespan(app: FastAPI):
|
| 28 |
+
"""Lifespan context manager for startup/shutdown events"""
|
| 29 |
+
# Startup
|
| 30 |
+
global rag_system
|
| 31 |
+
try:
|
| 32 |
+
logger.info("🚀 Starting Well Being Agent...")
|
| 33 |
+
|
| 34 |
+
# Import here to avoid circular imports
|
| 35 |
+
from Agent import load_index, BreastCancerRAGSystem
|
| 36 |
+
|
| 37 |
+
logger.info("📋 Loading configuration and index...")
|
| 38 |
+
|
| 39 |
+
# Add delay to ensure everything is loaded
|
| 40 |
+
await asyncio.sleep(2)
|
| 41 |
+
|
| 42 |
+
index, retriever = load_index()
|
| 43 |
+
if index and retriever:
|
| 44 |
+
rag_system = BreastCancerRAGSystem(index, retriever)
|
| 45 |
+
logger.info("✅ RAG System initialized successfully")
|
| 46 |
+
|
| 47 |
+
# Test the system
|
| 48 |
+
try:
|
| 49 |
+
test_answer = rag_system.get_enhanced_answer("Hello, are you working?")
|
| 50 |
+
logger.info(f"✅ System test successful: {test_answer[:50]}...")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.warning(f"⚠️ System test failed: {e}")
|
| 53 |
+
|
| 54 |
+
else:
|
| 55 |
+
logger.error("❌ Failed to load index - system will not work properly")
|
| 56 |
+
rag_system = None
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"❌ Startup error: {e}")
|
| 60 |
+
import traceback
|
| 61 |
+
traceback.print_exc()
|
| 62 |
+
|
| 63 |
+
yield
|
| 64 |
+
|
| 65 |
+
# Shutdown
|
| 66 |
+
logger.info("🛑 Shutting down Well Being Agent...")
|
| 67 |
+
|
| 68 |
+
app = FastAPI(
|
| 69 |
+
title="Well Being Agent - Breast Cancer Support",
|
| 70 |
+
description="AI-powered breast cancer support system providing evidence-based information and emotional support",
|
| 71 |
+
version="1.0.0",
|
| 72 |
+
lifespan=lifespan
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Add CORS middleware
|
| 76 |
+
app.add_middleware(
|
| 77 |
+
CORSMiddleware,
|
| 78 |
+
allow_origins=["*"],
|
| 79 |
+
allow_credentials=True,
|
| 80 |
+
allow_methods=["*"],
|
| 81 |
+
allow_headers=["*"],
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
class QueryRequest(BaseModel):
|
| 85 |
+
query: str
|
| 86 |
+
language: str = "auto"
|
| 87 |
+
response_type: str = "text"
|
| 88 |
+
|
| 89 |
+
class QueryResponse(BaseModel):
|
| 90 |
+
answer: str
|
| 91 |
+
status: str
|
| 92 |
+
language: str = "english"
|
| 93 |
+
|
| 94 |
+
class VoiceResponse(BaseModel):
|
| 95 |
+
text: str
|
| 96 |
+
language: str = "english"
|
| 97 |
+
status: str = "success"
|
| 98 |
+
|
| 99 |
+
# Create directories if they don't exist
|
| 100 |
+
os.makedirs("static/audio", exist_ok=True)
|
| 101 |
+
logger.info(f"📁 Created directory structure: static/audio")
|
| 102 |
+
logger.info(f"📁 Current working directory: {os.getcwd()}")
|
| 103 |
+
|
| 104 |
+
# Serve static files
|
| 105 |
+
try:
|
| 106 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 107 |
+
logger.info("✅ Static files mounted successfully")
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"❌ Failed to mount static files: {e}")
|
| 110 |
+
|
| 111 |
+
# Frontend serving
|
| 112 |
+
@app.get("/")
|
| 113 |
+
async def serve_frontend():
|
| 114 |
+
"""Serve the main frontend page"""
|
| 115 |
+
try:
|
| 116 |
+
if not os.path.exists('index.html'):
|
| 117 |
+
logger.error("❌ index.html not found!")
|
| 118 |
+
fallback_html = """
|
| 119 |
+
<!DOCTYPE html>
|
| 120 |
+
<html>
|
| 121 |
+
<head>
|
| 122 |
+
<title>Well Being Agent - System Running</title>
|
| 123 |
+
<style>
|
| 124 |
+
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
|
| 125 |
+
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
|
| 126 |
+
.status { color: green; font-weight: bold; }
|
| 127 |
+
.error { color: red; }
|
| 128 |
+
</style>
|
| 129 |
+
</head>
|
| 130 |
+
<body>
|
| 131 |
+
<div class="container">
|
| 132 |
+
<h1>🚀 Well Being Agent - Backend Running</h1>
|
| 133 |
+
<p class="status">✅ Server is running successfully</p>
|
| 134 |
+
<p class="error">⚠️ index.html file not found</p>
|
| 135 |
+
<p>Current directory: """ + os.getcwd() + """</p>
|
| 136 |
+
<p>Static audio directory: """ + str(os.path.exists('static/audio')) + """</p>
|
| 137 |
+
</div>
|
| 138 |
+
</body>
|
| 139 |
+
</html>
|
| 140 |
+
"""
|
| 141 |
+
return HTMLResponse(content=fallback_html, status_code=200)
|
| 142 |
+
|
| 143 |
+
return FileResponse('index.html')
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
logger.error(f"❌ Error serving frontend: {e}")
|
| 147 |
+
return JSONResponse(
|
| 148 |
+
{"error": "Frontend serving failed", "details": str(e)},
|
| 149 |
+
status_code=500
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
@app.get("/styles.css")
|
| 153 |
+
async def serve_css():
|
| 154 |
+
"""Serve CSS file"""
|
| 155 |
+
try:
|
| 156 |
+
if os.path.exists('styles.css'):
|
| 157 |
+
return FileResponse('styles.css', media_type='text/css')
|
| 158 |
+
else:
|
| 159 |
+
return JSONResponse({"error": "CSS file not found"}, status_code=404)
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return JSONResponse({"error": "CSS serving failed"}, status_code=500)
|
| 162 |
+
|
| 163 |
+
@app.get("/script.js")
|
| 164 |
+
async def serve_js():
|
| 165 |
+
"""Serve JavaScript file"""
|
| 166 |
+
try:
|
| 167 |
+
if os.path.exists('script.js'):
|
| 168 |
+
return FileResponse('script.js', media_type='application/javascript')
|
| 169 |
+
else:
|
| 170 |
+
return JSONResponse({"error": "JavaScript file not found"}, status_code=404)
|
| 171 |
+
except Exception as e:
|
| 172 |
+
return JSONResponse({"error": "JavaScript serving failed"}, status_code=500)
|
| 173 |
+
|
| 174 |
+
@app.post("/ask-query", response_model=QueryResponse)
|
| 175 |
+
async def ask_query(request: QueryRequest):
|
| 176 |
+
"""Main endpoint for processing queries"""
|
| 177 |
+
try:
|
| 178 |
+
if not rag_system:
|
| 179 |
+
return QueryResponse(
|
| 180 |
+
answer="I'm currently initializing. Please wait a moment and try again.",
|
| 181 |
+
status="error",
|
| 182 |
+
language="english"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
if not request.query or not request.query.strip():
|
| 186 |
+
return QueryResponse(
|
| 187 |
+
answer="Please enter a question about breast cancer support.",
|
| 188 |
+
status="error",
|
| 189 |
+
language="english"
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Determine language
|
| 193 |
+
if request.language == "auto":
|
| 194 |
+
detected_language = rag_system.detect_language(request.query)
|
| 195 |
+
else:
|
| 196 |
+
detected_language = request.language
|
| 197 |
+
|
| 198 |
+
logger.info(f"🌐 Processing query in {detected_language}, Type: {request.response_type}")
|
| 199 |
+
|
| 200 |
+
# Process the query with response type
|
| 201 |
+
answer = rag_system.get_enhanced_answer(
|
| 202 |
+
user_query=request.query,
|
| 203 |
+
language=detected_language,
|
| 204 |
+
response_type=request.response_type
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
return QueryResponse(
|
| 208 |
+
answer=answer,
|
| 209 |
+
status="success",
|
| 210 |
+
language=detected_language
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
except Exception as e:
|
| 214 |
+
logger.error(f"Error processing query: {e}")
|
| 215 |
+
return QueryResponse(
|
| 216 |
+
answer="I apologize, but I'm having trouble processing your request right now. Please try again in a moment.",
|
| 217 |
+
status="error",
|
| 218 |
+
language="english"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
@app.post("/voice-query", response_model=VoiceResponse)
|
| 222 |
+
async def process_voice_query(
|
| 223 |
+
file: UploadFile = File(...),
|
| 224 |
+
language: str = "auto" # Auto-detect language from speech
|
| 225 |
+
):
|
| 226 |
+
"""Process voice query and return TEXT response only (English & Urdu)"""
|
| 227 |
+
try:
|
| 228 |
+
# Validate file type
|
| 229 |
+
if not file.content_type or not file.content_type.startswith('audio/'):
|
| 230 |
+
raise HTTPException(status_code=400, detail="File must be an audio file")
|
| 231 |
+
|
| 232 |
+
logger.info(f"🎤 Processing voice query - Language preference: {language}")
|
| 233 |
+
|
| 234 |
+
# Import audio processor with proper error handling
|
| 235 |
+
try:
|
| 236 |
+
from audio_processor import audio_processor
|
| 237 |
+
except ImportError as e:
|
| 238 |
+
logger.error(f"❌ Failed to import audio_processor: {e}")
|
| 239 |
+
return VoiceResponse(
|
| 240 |
+
text="Audio processing service is currently unavailable.",
|
| 241 |
+
status="error",
|
| 242 |
+
language="english"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Convert speech to text with language detection
|
| 246 |
+
stt_result = await audio_processor.speech_to_text(file, language)
|
| 247 |
+
|
| 248 |
+
if not stt_result or not stt_result.get('text'):
|
| 249 |
+
raise HTTPException(status_code=400, detail="Could not transcribe audio")
|
| 250 |
+
|
| 251 |
+
query_text = stt_result['text']
|
| 252 |
+
detected_language = stt_result.get('language', 'english')
|
| 253 |
+
|
| 254 |
+
logger.info(f"📝 Transcribed text ({detected_language}): {query_text}")
|
| 255 |
+
|
| 256 |
+
# Process the query through RAG system
|
| 257 |
+
if not rag_system:
|
| 258 |
+
return VoiceResponse(
|
| 259 |
+
text="System is initializing. Please try again in a moment.",
|
| 260 |
+
status="error",
|
| 261 |
+
language=detected_language
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# ✅ Always use TEXT response type for voice queries
|
| 265 |
+
answer = rag_system.get_enhanced_answer(
|
| 266 |
+
user_query=query_text,
|
| 267 |
+
language=detected_language, # Use detected language
|
| 268 |
+
response_type="text" # Always text response
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
logger.info(f"✅ Voice query processed successfully - Response in {detected_language}")
|
| 272 |
+
|
| 273 |
+
return VoiceResponse(
|
| 274 |
+
text=answer, # Always return text
|
| 275 |
+
language=detected_language,
|
| 276 |
+
status="success"
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
except HTTPException:
|
| 280 |
+
raise
|
| 281 |
+
except Exception as e:
|
| 282 |
+
logger.error(f"Error processing voice query: {e}")
|
| 283 |
+
return VoiceResponse(
|
| 284 |
+
text="Sorry, I encountered an error processing your voice message.",
|
| 285 |
+
status="error",
|
| 286 |
+
language="english"
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Audio serving endpoint (kept for any future use)
|
| 290 |
+
@app.get("/audio/{filename}")
|
| 291 |
+
async def serve_audio_direct(filename: str):
|
| 292 |
+
"""Direct audio serving endpoint"""
|
| 293 |
+
try:
|
| 294 |
+
audio_path = os.path.join("static", "audio", filename)
|
| 295 |
+
logger.info(f"🔍 Direct audio request for: {filename}")
|
| 296 |
+
|
| 297 |
+
if not os.path.exists(audio_path):
|
| 298 |
+
logger.error(f"❌ Audio file not found: {audio_path}")
|
| 299 |
+
raise HTTPException(status_code=404, detail=f"Audio file {filename} not found")
|
| 300 |
+
|
| 301 |
+
# Determine content type
|
| 302 |
+
if filename.endswith('.mp3'):
|
| 303 |
+
media_type = "audio/mpeg"
|
| 304 |
+
elif filename.endswith('.wav'):
|
| 305 |
+
media_type = "audio/wav"
|
| 306 |
+
else:
|
| 307 |
+
media_type = "audio/mpeg"
|
| 308 |
+
|
| 309 |
+
logger.info(f"🔊 Serving audio file: {audio_path}")
|
| 310 |
+
return FileResponse(audio_path, media_type=media_type, filename=filename)
|
| 311 |
+
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"❌ Error serving audio file: {e}")
|
| 314 |
+
raise HTTPException(status_code=500, detail="Error serving audio file")
|
| 315 |
+
|
| 316 |
+
@app.get("/debug-audio")
|
| 317 |
+
async def debug_audio():
|
| 318 |
+
"""Debug endpoint to check audio file locations"""
|
| 319 |
+
import glob
|
| 320 |
+
|
| 321 |
+
audio_info = {
|
| 322 |
+
"current_directory": os.getcwd(),
|
| 323 |
+
"static_directory_exists": os.path.exists("static"),
|
| 324 |
+
"static_audio_exists": os.path.exists("static/audio"),
|
| 325 |
+
"audio_files_in_static_audio": [],
|
| 326 |
+
"static_files_mounted": True
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
# Check static/audio directory
|
| 330 |
+
if os.path.exists("static/audio"):
|
| 331 |
+
audio_files = glob.glob("static/audio/*.mp3") + glob.glob("static/audio/*.wav")
|
| 332 |
+
audio_info["audio_files_in_static_audio"] = [
|
| 333 |
+
{
|
| 334 |
+
"name": os.path.basename(f),
|
| 335 |
+
"size": os.path.getsize(f),
|
| 336 |
+
"path": f,
|
| 337 |
+
"absolute_path": os.path.abspath(f),
|
| 338 |
+
}
|
| 339 |
+
for f in audio_files
|
| 340 |
+
]
|
| 341 |
+
|
| 342 |
+
return JSONResponse(audio_info)
|
| 343 |
+
|
| 344 |
+
@app.get("/predefined-questions")
|
| 345 |
+
async def get_predefined_questions(language: str = "english"):
|
| 346 |
+
"""Get predefined questions for breast cancer patients"""
|
| 347 |
+
try:
|
| 348 |
+
if not rag_system:
|
| 349 |
+
return JSONResponse({
|
| 350 |
+
"questions": [],
|
| 351 |
+
"status": "system_initializing"
|
| 352 |
+
})
|
| 353 |
+
|
| 354 |
+
questions = rag_system.get_predefined_questions(language)
|
| 355 |
+
return JSONResponse({
|
| 356 |
+
"questions": questions,
|
| 357 |
+
"status": "success",
|
| 358 |
+
"language": language
|
| 359 |
+
})
|
| 360 |
+
|
| 361 |
+
except Exception as e:
|
| 362 |
+
logger.error(f"Error getting predefined questions: {e}")
|
| 363 |
+
return JSONResponse({
|
| 364 |
+
"questions": [],
|
| 365 |
+
"status": "error"
|
| 366 |
+
})
|
| 367 |
+
|
| 368 |
+
@app.get("/health")
|
| 369 |
+
async def health_check():
|
| 370 |
+
"""Health check endpoint"""
|
| 371 |
+
health_status = {
|
| 372 |
+
"status": "healthy" if rag_system else "initializing",
|
| 373 |
+
"rag_system_loaded": rag_system is not None,
|
| 374 |
+
"service": "Well Being Agent - Breast Cancer Support",
|
| 375 |
+
"version": "1.0.0"
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
return JSONResponse(health_status)
|
| 379 |
+
|
| 380 |
+
@app.get("/info")
|
| 381 |
+
async def system_info():
|
| 382 |
+
"""System information endpoint"""
|
| 383 |
+
info = {
|
| 384 |
+
"name": "Well Being Agent",
|
| 385 |
+
"description": "AI-powered breast cancer support system",
|
| 386 |
+
"version": "1.0.0",
|
| 387 |
+
"status": "ready" if rag_system else "initializing",
|
| 388 |
+
"features": [
|
| 389 |
+
"Breast cancer information",
|
| 390 |
+
"Treatment guidance",
|
| 391 |
+
"Fertility options",
|
| 392 |
+
"Recovery timelines",
|
| 393 |
+
"Emotional support",
|
| 394 |
+
"Multilingual support (English/Urdu)",
|
| 395 |
+
"Voice query support (Text responses)"
|
| 396 |
+
]
|
| 397 |
+
}
|
| 398 |
+
return JSONResponse(info)
|
| 399 |
+
|
| 400 |
+
# Debug endpoint to check file existence
|
| 401 |
+
@app.get("/debug-files")
|
| 402 |
+
async def debug_files():
|
| 403 |
+
"""Check if required files exist"""
|
| 404 |
+
files = {
|
| 405 |
+
'index.html': os.path.exists('index.html'),
|
| 406 |
+
'styles.css': os.path.exists('styles.css'),
|
| 407 |
+
'script.js': os.path.exists('script.js'),
|
| 408 |
+
'Agent.py': os.path.exists('Agent.py'),
|
| 409 |
+
'audio_processor.py': os.path.exists('audio_processor.py'),
|
| 410 |
+
'current_directory': os.getcwd()
|
| 411 |
+
}
|
| 412 |
+
return JSONResponse(files)
|
| 413 |
+
|
| 414 |
+
async def cleanup_old_audio_files():
|
| 415 |
+
"""Clean up audio files older than 1 hour"""
|
| 416 |
+
while True:
|
| 417 |
+
try:
|
| 418 |
+
audio_dir = os.path.join("static", "audio")
|
| 419 |
+
if os.path.exists(audio_dir):
|
| 420 |
+
current_time = time.time()
|
| 421 |
+
for filename in os.listdir(audio_dir):
|
| 422 |
+
file_path = os.path.join(audio_dir, filename)
|
| 423 |
+
if os.path.isfile(file_path):
|
| 424 |
+
# Delete files older than 1 hour
|
| 425 |
+
if current_time - os.path.getctime(file_path) > 3600:
|
| 426 |
+
os.remove(file_path)
|
| 427 |
+
logger.info(f"🧹 Cleaned up old audio file: {filename}")
|
| 428 |
+
except Exception as e:
|
| 429 |
+
logger.error(f"Error cleaning up audio files: {e}")
|
| 430 |
+
|
| 431 |
+
await asyncio.sleep(3600)
|
| 432 |
+
|
| 433 |
+
# Start cleanup task when app starts
|
| 434 |
+
@app.on_event("startup")
|
| 435 |
+
async def start_cleanup_task():
|
| 436 |
+
asyncio.create_task(cleanup_old_audio_files())
|
| 437 |
+
|
| 438 |
+
# Fallback route for SPA
|
| 439 |
+
@app.get("/{full_path:path}")
|
| 440 |
+
async def serve_frontend_fallback(full_path: str):
|
| 441 |
+
"""Fallback to serve index.html for SPA routing"""
|
| 442 |
+
if os.path.exists(full_path) and full_path != "":
|
| 443 |
+
return FileResponse(full_path)
|
| 444 |
+
return FileResponse('index.html')
|
| 445 |
+
|
| 446 |
+
if __name__ == "__main__":
|
| 447 |
+
import uvicorn
|
| 448 |
+
# Use port 7860 for Hugging Face, 8000 for local development
|
| 449 |
+
port = int(os.environ.get("PORT", 7860)) # CHANGED: Default to 7860
|
| 450 |
+
logger.info(f"🌐 Starting Well Being Agent Server on port {port}...")
|
| 451 |
+
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
|
audio_processor.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# audio_processor.py - FREE TTS and STT for English AND Urdu voice notes
|
| 2 |
+
import os
|
| 3 |
+
import tempfile
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
+
from fastapi import HTTPException, UploadFile
|
| 8 |
+
import uuid
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
class AudioProcessor:
|
| 14 |
+
"""FREE Audio processing system for STT and TTS functionality (English + Urdu ONLY)"""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.supported_languages = ["english", "urdu"]
|
| 18 |
+
logger.info("🎵 FREE Audio Processor initialized - Supporting English & Urdu ONLY")
|
| 19 |
+
|
| 20 |
+
async def speech_to_text(self, audio_file: UploadFile, language: str = "auto") -> Dict[str, Any]:
|
| 21 |
+
"""
|
| 22 |
+
Convert speech to text using FREE STT services for English AND Urdu ONLY
|
| 23 |
+
"""
|
| 24 |
+
try:
|
| 25 |
+
logger.info(f"🎤 Converting speech to text - Language: {language}")
|
| 26 |
+
|
| 27 |
+
# Read audio file
|
| 28 |
+
audio_content = await audio_file.read()
|
| 29 |
+
|
| 30 |
+
# Try local Whisper for multilingual support
|
| 31 |
+
stt_result = await self._try_whisper_stt(audio_content, language)
|
| 32 |
+
if stt_result:
|
| 33 |
+
# Verify detected language is only Urdu or English
|
| 34 |
+
detected_language = self._strict_detect_language_from_text(stt_result["text"])
|
| 35 |
+
if detected_language not in ["english", "urdu"]:
|
| 36 |
+
logger.warning(f"⚠️ Detected non-supported language: {detected_language}, treating as English")
|
| 37 |
+
detected_language = "english"
|
| 38 |
+
|
| 39 |
+
stt_result["language"] = detected_language
|
| 40 |
+
return stt_result
|
| 41 |
+
|
| 42 |
+
# Fallback to SpeechRecognition with Google Web API (mainly English)
|
| 43 |
+
stt_result = await self._try_speech_recognition(audio_content)
|
| 44 |
+
if stt_result:
|
| 45 |
+
detected_language = self._strict_detect_language_from_text(stt_result["text"])
|
| 46 |
+
if detected_language not in ["english", "urdu"]:
|
| 47 |
+
detected_language = "english"
|
| 48 |
+
|
| 49 |
+
stt_result["language"] = detected_language
|
| 50 |
+
return stt_result
|
| 51 |
+
|
| 52 |
+
raise HTTPException(status_code=400, detail="No FREE STT service available")
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logger.error(f"❌ STT Error: {e}")
|
| 56 |
+
raise HTTPException(status_code=500, detail=f"Speech recognition failed: {str(e)}")
|
| 57 |
+
|
| 58 |
+
async def _try_whisper_stt(self, audio_content: bytes, language: str = "auto") -> Optional[Dict[str, Any]]:
|
| 59 |
+
"""Try local Whisper model with strict language filtering"""
|
| 60 |
+
try:
|
| 61 |
+
import whisper
|
| 62 |
+
|
| 63 |
+
# Create temporary file
|
| 64 |
+
with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as temp_audio:
|
| 65 |
+
temp_audio.write(audio_content)
|
| 66 |
+
temp_audio_path = temp_audio.name
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
logger.info("🔊 Using local Whisper (English/Urdu)...")
|
| 70 |
+
model = whisper.load_model("base")
|
| 71 |
+
|
| 72 |
+
# Set language parameter for Whisper - only allow English or Urdu
|
| 73 |
+
whisper_language = None
|
| 74 |
+
if language == "urdu":
|
| 75 |
+
whisper_language = "urdu"
|
| 76 |
+
elif language == "english":
|
| 77 |
+
whisper_language = "english"
|
| 78 |
+
# For "auto", let Whisper detect but we'll filter later
|
| 79 |
+
|
| 80 |
+
result = model.transcribe(temp_audio_path, language=whisper_language)
|
| 81 |
+
|
| 82 |
+
# Apply strict language detection
|
| 83 |
+
detected_language = self._strict_detect_language_from_text(result["text"])
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
"text": result["text"].strip(),
|
| 87 |
+
"language": detected_language,
|
| 88 |
+
"service": "local_whisper",
|
| 89 |
+
"confidence": 0.8
|
| 90 |
+
}
|
| 91 |
+
finally:
|
| 92 |
+
# Ensure temp file cleanup
|
| 93 |
+
if os.path.exists(temp_audio_path):
|
| 94 |
+
try:
|
| 95 |
+
os.unlink(temp_audio_path)
|
| 96 |
+
except Exception as cleanup_error:
|
| 97 |
+
logger.warning(f"⚠️ Failed to cleanup temp file: {cleanup_error}")
|
| 98 |
+
|
| 99 |
+
except ImportError:
|
| 100 |
+
logger.warning("Whisper not available for local STT")
|
| 101 |
+
return None
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.warning(f"Local Whisper STT failed: {e}")
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
async def _try_speech_recognition(self, audio_content: bytes) -> Optional[Dict[str, Any]]:
|
| 107 |
+
"""Try SpeechRecognition with Google Web API (mainly English)"""
|
| 108 |
+
try:
|
| 109 |
+
import speech_recognition as sr
|
| 110 |
+
from pydub import AudioSegment
|
| 111 |
+
import io
|
| 112 |
+
|
| 113 |
+
# Convert webm to wav for SpeechRecognition
|
| 114 |
+
audio = AudioSegment.from_file(io.BytesIO(audio_content), format="webm")
|
| 115 |
+
wav_data = io.BytesIO()
|
| 116 |
+
audio.export(wav_data, format="wav")
|
| 117 |
+
wav_data.seek(0)
|
| 118 |
+
|
| 119 |
+
recognizer = sr.Recognizer()
|
| 120 |
+
|
| 121 |
+
with sr.AudioFile(wav_data) as source:
|
| 122 |
+
audio_data = recognizer.record(source)
|
| 123 |
+
text = recognizer.recognize_google(audio_data)
|
| 124 |
+
|
| 125 |
+
# Apply strict language detection
|
| 126 |
+
detected_language = self._strict_detect_language_from_text(text)
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"text": text,
|
| 130 |
+
"language": detected_language,
|
| 131 |
+
"service": "google_web_api",
|
| 132 |
+
"confidence": 0.7
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
except ImportError:
|
| 136 |
+
logger.warning("SpeechRecognition not available")
|
| 137 |
+
return None
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.warning(f"SpeechRecognition failed: {e}")
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
def _strict_detect_language_from_text(self, text: str) -> str:
|
| 143 |
+
"""
|
| 144 |
+
Strict language detection that only identifies Urdu or English
|
| 145 |
+
Specifically excludes Hindi, Arabic, and other languages
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
text = text.strip()
|
| 149 |
+
if not text:
|
| 150 |
+
return "english" # Default to English for empty text
|
| 151 |
+
|
| 152 |
+
# === STRICT URDU DETECTION ===
|
| 153 |
+
# Urdu-specific character ranges (excluding Arabic and Hindi overlaps)
|
| 154 |
+
urdu_specific_ranges = [
|
| 155 |
+
r'[\u0679-\u0679]', # Urdu-specific letters
|
| 156 |
+
r'[\u067E-\u067E]', # Peh
|
| 157 |
+
r'[\u0686-\u0686]', # Cheh
|
| 158 |
+
r'[\u0688-\u0688]', # Ddal
|
| 159 |
+
r'[\u0691-\u0691]', # Rreh
|
| 160 |
+
r'[\u0698-\u0698]', # Jeh
|
| 161 |
+
r'[\u06A9-\u06A9]', # Keheh
|
| 162 |
+
r'[\u06AF-\u06AF]', # Gaf
|
| 163 |
+
r'[\u06BA-\u06BA]', # Noon Ghunna
|
| 164 |
+
r'[\u06BE-\u06BE]', # Heh Doachashmee
|
| 165 |
+
r'[\u06C1-\u06C1]', # Heh Goal
|
| 166 |
+
r'[\u06C2-\u06C2]', # Heh Goal with Hamza Above
|
| 167 |
+
r'[\u06CC-\u06CC]', # Farsi Yeh
|
| 168 |
+
r'[\u06D2-\u06D2]', # Yeh Barree
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
# Common Urdu words that are distinct from Hindi/Arabic
|
| 172 |
+
urdu_specific_words = [
|
| 173 |
+
'ہے', 'ہیں', 'ہوں', 'کیا', 'کے', 'کو', 'سے', 'پر', 'میں',
|
| 174 |
+
'اور', 'لیکن', 'اگر', 'تو', 'بھی', 'ہی', 'تھا', 'تھی',
|
| 175 |
+
'تھے', 'ہو', 'رہا', 'رہی', 'رہے', 'دیں', 'دی', 'دو', 'دیجیے',
|
| 176 |
+
'برائے', 'کےلیے', 'کےساتھ', 'کےبعد', 'کےپاس', 'کےنیچے'
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
# Check for Urdu-specific characters
|
| 180 |
+
urdu_char_count = 0
|
| 181 |
+
for pattern in urdu_specific_ranges:
|
| 182 |
+
urdu_char_count += len(re.findall(pattern, text))
|
| 183 |
+
|
| 184 |
+
# Check for Urdu-specific words
|
| 185 |
+
urdu_word_count = sum(1 for word in urdu_specific_words if word in text)
|
| 186 |
+
|
| 187 |
+
# Check for common Urdu sentence structures
|
| 188 |
+
urdu_indicators = [
|
| 189 |
+
' کا ', ' کی ', ' کے ', ' کو ', ' سے ', ' پر ', ' میں ', ' نے ',
|
| 190 |
+
' ہی ', ' بھی ', ' تو ', ' اگر ', ' لیکن ', ' اور ', ' یا '
|
| 191 |
+
]
|
| 192 |
+
urdu_structure_count = sum(1 for indicator in urdu_indicators if indicator in text)
|
| 193 |
+
|
| 194 |
+
# === HINDI EXCLUSION ===
|
| 195 |
+
# Hindi-specific characters and words to exclude
|
| 196 |
+
hindi_specific_chars = r'[\u0900-\u097F]' # Devanagari range
|
| 197 |
+
hindi_char_count = len(re.findall(hindi_specific_chars, text))
|
| 198 |
+
|
| 199 |
+
hindi_specific_words = ['है', 'हो', 'की', 'के', 'को', 'से', 'में', 'ना', 'नी', 'ने']
|
| 200 |
+
hindi_word_count = sum(1 for word in hindi_specific_words if word in text)
|
| 201 |
+
|
| 202 |
+
# === ARABIC EXCLUSION ===
|
| 203 |
+
# Arabic-specific characters (excluding common Urdu-Arabic overlaps)
|
| 204 |
+
arabic_specific_chars = r'[\uFE70-\uFEFF]' # Arabic presentation forms
|
| 205 |
+
arabic_char_count = len(re.findall(arabic_specific_chars, text))
|
| 206 |
+
|
| 207 |
+
# === ENGLISH DETECTION ===
|
| 208 |
+
english_words = [
|
| 209 |
+
'the', 'and', 'you', 'that', 'was', 'for', 'are', 'with', 'his', 'they',
|
| 210 |
+
'this', 'have', 'from', 'one', 'had', 'word', 'but', 'not', 'what', 'all',
|
| 211 |
+
'were', 'when', 'your', 'can', 'said', 'there', 'each', 'which', 'she', 'do',
|
| 212 |
+
'how', 'their', 'will', 'other', 'about', 'out', 'many', 'then', 'them', 'these'
|
| 213 |
+
]
|
| 214 |
+
text_lower = text.lower()
|
| 215 |
+
english_score = sum(1 for word in english_words if word in text_lower)
|
| 216 |
+
|
| 217 |
+
# === LANGUAGE DECISION LOGIC ===
|
| 218 |
+
|
| 219 |
+
# First, exclude Hindi and Arabic
|
| 220 |
+
if hindi_char_count > 2 or hindi_word_count > 1:
|
| 221 |
+
logger.info("🔍 Hindi detected, treating as English")
|
| 222 |
+
return "english"
|
| 223 |
+
|
| 224 |
+
if arabic_char_count > 2:
|
| 225 |
+
logger.info("🔍 Arabic detected, treating as English")
|
| 226 |
+
return "english"
|
| 227 |
+
|
| 228 |
+
# Then detect Urdu with high confidence
|
| 229 |
+
urdu_confidence_score = (
|
| 230 |
+
urdu_char_count * 2 +
|
| 231 |
+
urdu_word_count * 3 +
|
| 232 |
+
urdu_structure_count * 1.5
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Strong Urdu detection thresholds
|
| 236 |
+
if urdu_confidence_score >= 5:
|
| 237 |
+
logger.info(f"🔍 Urdu detected (confidence: {urdu_confidence_score})")
|
| 238 |
+
return "urdu"
|
| 239 |
+
|
| 240 |
+
# English detection
|
| 241 |
+
if english_score >= 3 or len(text.split()) >= 4:
|
| 242 |
+
logger.info(f"🔍 English detected (score: {english_score})")
|
| 243 |
+
return "english"
|
| 244 |
+
|
| 245 |
+
# If we have some Urdu indicators but not enough for confident detection
|
| 246 |
+
if urdu_confidence_score >= 2:
|
| 247 |
+
logger.info(f"🔍 Weak Urdu signals, treating as Urdu (confidence: {urdu_confidence_score})")
|
| 248 |
+
return "urdu"
|
| 249 |
+
|
| 250 |
+
# Default to English
|
| 251 |
+
logger.info("🔍 Defaulting to English")
|
| 252 |
+
return "english"
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
logger.error(f"❌ Language detection error: {e}")
|
| 256 |
+
return "english" # Safe default
|
| 257 |
+
|
| 258 |
+
def _detect_language_from_text(self, text: str) -> str:
|
| 259 |
+
"""Legacy method for backward compatibility"""
|
| 260 |
+
return self._strict_detect_language_from_text(text)
|
| 261 |
+
|
| 262 |
+
async def text_to_speech(self, text: str, language: str = "english") -> Optional[Dict[str, Any]]:
|
| 263 |
+
"""
|
| 264 |
+
Convert text to speech using FREE TTS services
|
| 265 |
+
NOTE: Keeping TTS for potential future use, but currently disabled for responses
|
| 266 |
+
"""
|
| 267 |
+
try:
|
| 268 |
+
# Since we're only returning text responses now, TTS is optional
|
| 269 |
+
# But keeping the function for potential future use
|
| 270 |
+
logger.info(f"🔊 TTS requested for {language}: {text[:50]}...")
|
| 271 |
+
return None # Disable TTS for now
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
logger.error(f"❌ TTS Error: {e}")
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
async def cleanup_old_audio_files(self, max_age_hours: int = 1):
|
| 278 |
+
"""Clean up audio files older than specified hours"""
|
| 279 |
+
try:
|
| 280 |
+
audio_dir = os.path.join("static", "audio")
|
| 281 |
+
if not os.path.exists(audio_dir):
|
| 282 |
+
return
|
| 283 |
+
|
| 284 |
+
current_time = time.time()
|
| 285 |
+
deleted_count = 0
|
| 286 |
+
|
| 287 |
+
for filename in os.listdir(audio_dir):
|
| 288 |
+
if filename.startswith("tts_") and (filename.endswith(".mp3") or filename.endswith(".wav")):
|
| 289 |
+
file_path = os.path.join(audio_dir, filename)
|
| 290 |
+
if os.path.isfile(file_path):
|
| 291 |
+
# Delete files older than max_age_hours
|
| 292 |
+
file_age_hours = (current_time - os.path.getctime(file_path)) / 3600
|
| 293 |
+
if file_age_hours > max_age_hours:
|
| 294 |
+
try:
|
| 295 |
+
os.remove(file_path)
|
| 296 |
+
deleted_count += 1
|
| 297 |
+
logger.info(f"🧹 Cleaned up old audio file: {filename}")
|
| 298 |
+
except Exception as cleanup_error:
|
| 299 |
+
logger.warning(f"⚠️ Failed to cleanup audio file {filename}: {cleanup_error}")
|
| 300 |
+
|
| 301 |
+
if deleted_count > 0:
|
| 302 |
+
logger.info(f"🧹 Cleaned up {deleted_count} old audio file(s)")
|
| 303 |
+
|
| 304 |
+
except Exception as e:
|
| 305 |
+
logger.error(f"Error cleaning up audio files: {e}")
|
| 306 |
+
|
| 307 |
+
# Global audio processor instance
|
| 308 |
+
audio_processor = AudioProcessor()
|
conversations.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"user_input": "Hello, are you working?",
|
| 4 |
+
"llm_response": "I'm here to support you. If you're considering pregnancy after breast cancer, it's generally recommended to wait 2-3 years after treatment. Fertility may be affected by chemotherapy, but many women are still able to conceive. Breastfeeding is possible after a lumpectomy, but may be more challenging after a mastectomy. Recovery timelines vary, but most women can return to normal activities within 6-8 weeks after surgery. Keep in mind that every individual's journey is unique, and it's essential to consider your specific situation and medical history when making decisions about pregnancy, fertility, and breastfeeding.\n\nRemember to discuss any concerns with your healthcare team.",
|
| 5 |
+
"language": "english",
|
| 6 |
+
"response_type": "text"
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"user_input": " Can you tell me about the skin?",
|
| 10 |
+
"llm_response": "\"Hello, I'm here to support you. Regarding your skin, during breast cancer treatment, you may experience changes such as dryness, redness, or sensitivity. This is often due to chemotherapy, radiation, or hormone therapy. To care for your skin, use gentle products, moisturize regularly, and avoid harsh soaps. It's also essential to protect your skin from the sun. Remember, these changes are usually temporary, and your skin will heal over time. You're not alone, and I'm here to support you through this journey.\"\n\nRemember to discuss any concerns with your healthcare team.",
|
| 11 |
+
"language": "english",
|
| 12 |
+
"response_type": "audio"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"timestamp": "2025-11-14T02:58:07.675170",
|
| 16 |
+
"user_input": "Hello, are you working?",
|
| 17 |
+
"llm_response": "Hello, I'm so glad you reached out, and yes, I am here to support you. I want you to know that you're not alone, and I'm committed to being a source of comfort and guidance for you. If you have any questions or concerns about breast cancer, I'm here to provide you with accurate and helpful information. Please feel free to share what's on your mind, and we can take things at your pace. Remember, you're taking a brave step by seeking support, and that's something to be proud of. I'm looking forward to being a part of your journey and offering any assistance I can.\n\nRemember to discuss any concerns with your healthcare team.",
|
| 18 |
+
"language": "english",
|
| 19 |
+
"response_type": "text"
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"timestamp": "2025-11-14T02:59:08.627291",
|
| 23 |
+
"user_input": "Hello, are you working?",
|
| 24 |
+
"llm_response": "Hello, I'm so glad you reached out, and yes, I am here to support you. I want you to know that you're not alone, and I'm committed to being a source of comfort and guidance for you. If you have any questions or concerns about breast cancer, I'm here to provide you with accurate and helpful information. Please feel free to share what's on your mind, and we can take things at your pace. Remember, you're taking a brave step by seeking support, and that's something to be proud of. I'm looking forward to being a part of your journey and offering any assistance I can.\n\nRemember to discuss any concerns with your healthcare team.",
|
| 25 |
+
"language": "english",
|
| 26 |
+
"response_type": "text"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"timestamp": "2025-11-14T02:59:32.398862",
|
| 30 |
+
"user_input": "کیموتھراپی کے دوران تھکاوٹ کیسے کم کریں؟",
|
| 31 |
+
"llm_response": "آپ جیسے بہادر لوگ ہی دنیا کو روشن کرتے ہیں۔\n\nکیموتھراپی کے دوران تھکاوٹ ایک عہوں مسئلہ، لیکن اسے کم کرنے کے لیے کچھ اقدامات کی جا سکت ی پلا اور ہوں قدم ی ک اپن ڈڈاکٹر سے مشور کری اور ان کی تجاویز پر عمل کری کچھ لوگ کیموتھراپی کے دوران تھکاوٹ کو کم کرنے کے لیے لائٹ ایکسرسائز، جیسے ک چل قدمیں یا یوگا، کو فائد مند پات ی مزید برآ، صحت مند غذا کھانا، پانی پینا، اور آرہوں کرنا بھی مددگار ثابت و سکتا اپن جسم کی سنن اور ضرورت پن پر آرہوں کرنے کی کوشش کری ی بھی یاد رکھی ک تھکاوٹ کیموتھراپی کا ایک عارضی اثر اور علاج ک ختم ون کے بعد ی کم و جائ گا۔ اپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکہیں۔\n\nاپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکھیں۔",
|
| 32 |
+
"language": "urdu",
|
| 33 |
+
"response_type": "text"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"timestamp": "2025-11-14T02:59:33.790744",
|
| 37 |
+
"user_input": "اگلے علاج کے بارے میں پریشانی کیسے دور کریں؟",
|
| 38 |
+
"llm_response": "آپ کی ہمت اور صبر کو سلام، بہتر دن ضرور آئیں گے۔\n\nآپ کا ے اگلے علاج کے بارے میں پریشانی کو دور کرنے کے لیے، میں آپ کا ا و یقین دلاتا ہوں کہ ہم آپ کا ے ساتھ ہیں اور ہر قدم پر آپ کا ا ی مدد کرنے کے لیے تیار ہیں۔ بریسٹ کینسر کے علاج کے مختلف طریقے ہیں، اور ہم ان تمہوں طریقوں کے بارے میں آپ کا ا و واضح اور درست معلومات فراہم کرنے کے لیے یہاں ہیں۔ ڈڈاکٹر اور ہمارے پروگرہوں کے ذریعے، آپ کا ا و بہترین ممکنہ دیکھ بھال ملے گی۔ جسے سے آپ کا ا و اپنے علاج کے بارے میں بہتر فیصلے لینے میں مدد ملے گی۔ میں آپ کا ا و یہ بھی یقین دلاتا ہوں کہ آپ اکیلے نہیں ہیں، ہم آپ کا ے ساتھ ہیں اور ہر قدم پر آپ کا ا ی مدد کرنے کے لیے تیار ہیں۔ اپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکہیں۔\n\nاپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکھیں۔",
|
| 39 |
+
"language": "urdu",
|
| 40 |
+
"response_type": "text"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"timestamp": "2025-11-14T02:59:36.254411",
|
| 44 |
+
"user_input": "ماسٹکٹومی کے بعد درد کیسے منظم کریں؟",
|
| 45 |
+
"llm_response": "یقین رکھیں، ہر طوفان کے بعد سکون ضرور آتا ہے۔\n\nماسٹکٹومیں کے بعد درد کو منظم کرنا ایک ہوں مرحل جو صحت یابی ک عمل میں مدد کرتا اسے دوران، ڈڈاکٹر کی تجویز پر درد ناک دوائی لینا ہوں مزید برآ، آرہوں کرنا، بریسٹ کینسر ک مریض کے لیے بت ضروری، کیونک جسم کو صحت یاب ون کے لیے وقت درکار وتا کچھ مریض физیو تھراپی بھی کروات ی جو درد کو کم کرنے اور جسمانی تحرک کو بہتر بنان میں مدد دیتی ی بھی مشور دیا جاتا ک مریض اپن جذبات کو بھی کنٹرول میں رکھی اور اگر ضرورت و تو نفسیاتی معاونت ل اسے طرح، و اپن جسمانی اور نفسیاتی صحت کو بہتر بنا سکت ی۔ اپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکہیں۔\n\nاپنی صحت کی دیکھ بھال ٹیم سے اپنے خدشات پر بات کرنا یاد رکھیں۔",
|
| 46 |
+
"language": "urdu",
|
| 47 |
+
"response_type": "text"
|
| 48 |
+
}
|
| 49 |
+
]
|
index.html
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Well Being Agent - Breast Cancer Support</title>
|
| 7 |
+
|
| 8 |
+
<!-- Optimized Urdu Fonts -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;500;600;700&family=Noto+Naskh+Arabic:wght@400;500;600;700&family=Scheherazade+New:wght@400;500;600;700&family=Lateef:wght@400;500;600;700&family=Amiri:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 14 |
+
<link rel="stylesheet" href="styles.css">
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<!-- Animated Background Elements -->
|
| 18 |
+
<div class="floating-elements">
|
| 19 |
+
<div class="floating-element el-1"></div>
|
| 20 |
+
<div class="floating-element el-2"></div>
|
| 21 |
+
<div class="floating-element el-3"></div>
|
| 22 |
+
<div class="floating-element el-4"></div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div class="container">
|
| 26 |
+
<!-- Header Section -->
|
| 27 |
+
<header class="header">
|
| 28 |
+
<div class="header-content">
|
| 29 |
+
<div class="logo-container">
|
| 30 |
+
<div class="logo">
|
| 31 |
+
<i class="fas fa-heartbeat"></i>
|
| 32 |
+
</div>
|
| 33 |
+
<h1 class="title">Well Being Agent</h1>
|
| 34 |
+
</div>
|
| 35 |
+
<p class="subtitle">Breast Cancer Support System</p>
|
| 36 |
+
<p class="compassion-message">Providing compassionate, evidence-based support for breast cancer patients and their families</p>
|
| 37 |
+
</div>
|
| 38 |
+
</header>
|
| 39 |
+
|
| 40 |
+
<!-- Main Content -->
|
| 41 |
+
<main class="main-content">
|
| 42 |
+
<!-- Quick Questions Section -->
|
| 43 |
+
<aside class="quick-questions">
|
| 44 |
+
<div class="section-header">
|
| 45 |
+
<h2 class="section-title">Quick Questions</h2>
|
| 46 |
+
<div class="decoration-line"></div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<!-- Tab Navigation -->
|
| 50 |
+
<div class="tabs-container">
|
| 51 |
+
<div class="tabs">
|
| 52 |
+
<button class="tab-btn active" data-tab="english">
|
| 53 |
+
<i class="fas fa-language"></i> English Questions
|
| 54 |
+
</button>
|
| 55 |
+
<button class="tab-btn" data-tab="urdu">
|
| 56 |
+
<i class="fas fa-language"></i> اردو سوالات
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<!-- English Questions Tab -->
|
| 61 |
+
<div class="tab-content active" id="english-tab">
|
| 62 |
+
<div class="questions-grid" id="english-questions">
|
| 63 |
+
<!-- Recent English questions will appear here dynamically -->
|
| 64 |
+
<div class="empty-state">
|
| 65 |
+
<i class="fas fa-comments"></i>
|
| 66 |
+
<p>No recent English questions yet. Start chatting to see them here!</p>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<!-- Urdu Questions Tab -->
|
| 72 |
+
<div class="tab-content" id="urdu-tab">
|
| 73 |
+
<div class="questions-grid urdu-questions" id="urdu-questions">
|
| 74 |
+
<!-- Recent Urdu questions will appear here dynamically -->
|
| 75 |
+
<div class="empty-state">
|
| 76 |
+
<i class="fas fa-comments"></i>
|
| 77 |
+
<p>ابھی تک کوئی اردو سوالات نہیں ہیں۔ بات چیت شروع کریں!</p>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</aside>
|
| 83 |
+
|
| 84 |
+
<!-- Chat Section -->
|
| 85 |
+
<section class="chat-section">
|
| 86 |
+
<div class="chat-container">
|
| 87 |
+
<!-- Chat Header -->
|
| 88 |
+
<div class="chat-header">
|
| 89 |
+
<div class="chat-profile">
|
| 90 |
+
<div class="profile-avatar">
|
| 91 |
+
<i class="fas fa-robot"></i>
|
| 92 |
+
<div class="online-indicator"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="profile-info">
|
| 95 |
+
<h3>Well Being Assistant</h3>
|
| 96 |
+
<p>Always here to support you</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="chat-stats">
|
| 100 |
+
<div class="stat">
|
| 101 |
+
<span class="stat-number" id="messageCount">0</span>
|
| 102 |
+
<span class="stat-label">Messages</span>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="stat">
|
| 105 |
+
<span class="stat-number" id="activeLanguage">English</span>
|
| 106 |
+
<span class="stat-label">Language</span>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- Chat Messages -->
|
| 112 |
+
<div class="chat-messages" id="chatMessages">
|
| 113 |
+
<div class="message system-message">
|
| 114 |
+
<div class="message-avatar">
|
| 115 |
+
<i class="fas fa-robot"></i>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="message-content">
|
| 118 |
+
<p>Hello! I'm your Well Being Agent. I'm here to provide supportive, evidence-based information about breast cancer. How can I help you today?</p>
|
| 119 |
+
<span class="message-time" id="welcomeTime"></span>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<!-- Typing Indicator -->
|
| 125 |
+
<div class="typing-indicator" id="typingIndicator">
|
| 126 |
+
<div class="typing-dots">
|
| 127 |
+
<span></span>
|
| 128 |
+
<span></span>
|
| 129 |
+
<span></span>
|
| 130 |
+
</div>
|
| 131 |
+
<p>Thinking of the best way to help you...</p>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- Chat Input -->
|
| 135 |
+
<div class="chat-input-container">
|
| 136 |
+
<div class="input-wrapper">
|
| 137 |
+
<input type="text" id="userInput" placeholder="Ask about breast cancer support, treatment options, or recovery...">
|
| 138 |
+
|
| 139 |
+
<!-- 🎤 New voice recording button -->
|
| 140 |
+
<button id="voiceButton" class="voice-button">
|
| 141 |
+
<i class="fas fa-microphone"></i>
|
| 142 |
+
</button>
|
| 143 |
+
<!-- ✉️ Existing send button -->
|
| 144 |
+
<button id="sendButton" class="send-button">
|
| 145 |
+
<i class="fas fa-paper-plane"></i>
|
| 146 |
+
<span class="button-text">Send</span>
|
| 147 |
+
</button>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="input-hint">
|
| 150 |
+
<i class="fas fa-lightbulb"></i>
|
| 151 |
+
<span>Current language: <strong id="currentLanguageDisplay">English</strong> - You can ask about symptoms, treatments, recovery, or emotional support</span>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</section>
|
| 156 |
+
</main>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<script src="script.js"></script>
|
| 160 |
+
</body>
|
| 161 |
+
</html>
|
language_utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# language_utils.py
|
| 2 |
+
import re
|
| 3 |
+
from langdetect import detect, LangDetectError
|
| 4 |
+
|
| 5 |
+
def detect_query_language(text: str) -> str:
|
| 6 |
+
"""
|
| 7 |
+
Detect if text is English or Urdu
|
| 8 |
+
Returns: 'english' or 'urdu'
|
| 9 |
+
"""
|
| 10 |
+
try:
|
| 11 |
+
# First check for Urdu characters (more reliable)
|
| 12 |
+
urdu_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+')
|
| 13 |
+
if urdu_pattern.search(text):
|
| 14 |
+
return 'urdu'
|
| 15 |
+
|
| 16 |
+
# Then use langdetect for other cases
|
| 17 |
+
detected_lang = detect(text)
|
| 18 |
+
return 'urdu' if detected_lang == 'ur' else 'english'
|
| 19 |
+
|
| 20 |
+
except LangDetectError:
|
| 21 |
+
return 'english'
|
| 22 |
+
except Exception:
|
| 23 |
+
return 'english'
|
| 24 |
+
|
| 25 |
+
def is_urdu_text(text: str) -> bool:
|
| 26 |
+
"""Check if text contains Urdu characters"""
|
| 27 |
+
urdu_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+')
|
| 28 |
+
return bool(urdu_pattern.search(text))
|
rag_system.log
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2025-11-10 22:28:46,853 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 2 |
+
2025-11-10 22:30:05,985 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 3 |
+
2025-11-10 22:49:31,018 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 4 |
+
2025-11-10 23:05:34,645 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 5 |
+
2025-11-11 17:57:12,543 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 6 |
+
2025-11-11 19:35:15,603 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 7 |
+
2025-11-11 19:35:28,651 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 8 |
+
2025-11-11 21:33:02,584 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 9 |
+
2025-11-11 23:10:45,825 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 10 |
+
2025-11-11 23:33:40,189 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 11 |
+
2025-11-12 00:13:26,990 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 12 |
+
2025-11-12 18:01:31,774 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 13 |
+
2025-11-12 18:43:39,916 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 14 |
+
2025-11-12 19:08:33,936 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 15 |
+
2025-11-12 19:24:28,082 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 16 |
+
2025-11-12 20:50:23,292 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 17 |
+
2025-11-12 20:58:54,128 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 18 |
+
2025-11-12 22:19:25,719 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
| 19 |
+
2025-11-14 02:57:25,961 - INFO - Load pretrained SentenceTransformer: sentence-transformers/all-MiniLM-L6-v2
|
script.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// script.js - Voice queries now return text responses only
|
| 2 |
+
// DOM Elements
|
| 3 |
+
const chatMessages = document.getElementById('chatMessages');
|
| 4 |
+
const userInput = document.getElementById('userInput');
|
| 5 |
+
const sendButton = document.getElementById('sendButton');
|
| 6 |
+
const voiceButton = document.getElementById('voiceButton');
|
| 7 |
+
const typingIndicator = document.getElementById('typingIndicator');
|
| 8 |
+
const messageCount = document.getElementById('messageCount');
|
| 9 |
+
const activeLanguage = document.getElementById('activeLanguage');
|
| 10 |
+
const currentLanguageDisplay = document.getElementById('currentLanguageDisplay');
|
| 11 |
+
const welcomeTime = document.getElementById('welcomeTime');
|
| 12 |
+
const englishQuestions = document.getElementById('english-questions');
|
| 13 |
+
const urduQuestions = document.getElementById('urdu-questions');
|
| 14 |
+
|
| 15 |
+
// State Management
|
| 16 |
+
let currentLanguage = 'english';
|
| 17 |
+
let messageCounter = 0;
|
| 18 |
+
let mediaRecorder;
|
| 19 |
+
let audioChunks = [];
|
| 20 |
+
let isRecording = false;
|
| 21 |
+
|
| 22 |
+
// Initialize
|
| 23 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 24 |
+
initializeApp();
|
| 25 |
+
setupEventListeners();
|
| 26 |
+
setWelcomeTime();
|
| 27 |
+
initializeWelcomeMessage();
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
async function initializeApp() {
|
| 31 |
+
updateLanguageDisplay();
|
| 32 |
+
updateInputPlaceholder();
|
| 33 |
+
await loadPredefinedQuestions();
|
| 34 |
+
updateQuestionsDisplay();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function setupEventListeners() {
|
| 38 |
+
// Send message on button click
|
| 39 |
+
sendButton.addEventListener('click', sendMessage);
|
| 40 |
+
|
| 41 |
+
// Send message on Enter key
|
| 42 |
+
userInput.addEventListener('keypress', function(e) {
|
| 43 |
+
if (e.key === 'Enter') {
|
| 44 |
+
sendMessage();
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// Tab switching
|
| 49 |
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
| 50 |
+
btn.addEventListener('click', function() {
|
| 51 |
+
const tabId = this.getAttribute('data-tab');
|
| 52 |
+
switchTab(tabId);
|
| 53 |
+
});
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
// Voice button listener
|
| 57 |
+
if (voiceButton) {
|
| 58 |
+
voiceButton.addEventListener('click', async () => {
|
| 59 |
+
if (!isRecording) {
|
| 60 |
+
startRecording();
|
| 61 |
+
} else {
|
| 62 |
+
stopRecording();
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
userInput.focus();
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function initializeWelcomeMessage() {
|
| 71 |
+
// Ensure the welcome message has proper styling and is visible
|
| 72 |
+
const welcomeMessage = document.querySelector('.system-message');
|
| 73 |
+
if (welcomeMessage) {
|
| 74 |
+
welcomeMessage.style.opacity = '1';
|
| 75 |
+
welcomeMessage.style.transform = 'translateY(0) scale(1)';
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function setWelcomeTime() {
|
| 80 |
+
const now = new Date();
|
| 81 |
+
welcomeTime.textContent = now.toLocaleTimeString([], {
|
| 82 |
+
hour: '2-digit',
|
| 83 |
+
minute: '2-digit'
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function switchTab(tabId) {
|
| 88 |
+
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
| 89 |
+
document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
|
| 90 |
+
|
| 91 |
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| 92 |
+
document.getElementById(`${tabId}-tab`).classList.add('active');
|
| 93 |
+
|
| 94 |
+
const newLanguage = tabId === 'urdu' ? 'urdu' : 'english';
|
| 95 |
+
if (currentLanguage !== newLanguage) {
|
| 96 |
+
currentLanguage = newLanguage;
|
| 97 |
+
updateLanguageDisplay();
|
| 98 |
+
updateInputPlaceholder();
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function updateLanguageDisplay() {
|
| 103 |
+
activeLanguage.textContent = currentLanguage === 'urdu' ? 'اردو' : 'English';
|
| 104 |
+
currentLanguageDisplay.textContent = currentLanguage === 'urdu' ? 'Urdu' : 'English';
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function updateInputPlaceholder() {
|
| 108 |
+
if (currentLanguage === 'urdu') {
|
| 109 |
+
userInput.placeholder = 'بریسٹ کینسر کے بارے میں پوچھیں...';
|
| 110 |
+
userInput.style.direction = 'rtl';
|
| 111 |
+
userInput.style.textAlign = 'right';
|
| 112 |
+
} else {
|
| 113 |
+
userInput.placeholder = 'Ask about breast cancer support, treatment options, or recovery...';
|
| 114 |
+
userInput.style.direction = 'ltr';
|
| 115 |
+
userInput.style.textAlign = 'left';
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async function loadPredefinedQuestions() {
|
| 120 |
+
try {
|
| 121 |
+
const englishResponse = await fetch('/predefined-questions?language=english');
|
| 122 |
+
const englishData = await englishResponse.json();
|
| 123 |
+
if (englishData.status === 'success') {
|
| 124 |
+
window.predefinedEnglishQuestions = englishData.questions;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const urduResponse = await fetch('/predefined-questions?language=urdu');
|
| 128 |
+
const urduData = await urduResponse.json();
|
| 129 |
+
if (urduData.status === 'success') {
|
| 130 |
+
window.predefinedUrduQuestions = urduData.questions;
|
| 131 |
+
}
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.error('Error loading predefined questions:', error);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function updateQuestionsDisplay() {
|
| 138 |
+
updateQuestionList(englishQuestions, 'english');
|
| 139 |
+
updateQuestionList(urduQuestions, 'urdu');
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function updateQuestionList(container, language) {
|
| 143 |
+
container.innerHTML = '';
|
| 144 |
+
const predefinedQuestions = language === 'urdu' ?
|
| 145 |
+
window.predefinedUrduQuestions :
|
| 146 |
+
window.predefinedEnglishQuestions;
|
| 147 |
+
|
| 148 |
+
if (predefinedQuestions && predefinedQuestions.length > 0) {
|
| 149 |
+
predefinedQuestions.forEach((questionData) => {
|
| 150 |
+
const questionCard = createQuestionCard(questionData, language);
|
| 151 |
+
container.appendChild(questionCard);
|
| 152 |
+
});
|
| 153 |
+
} else {
|
| 154 |
+
const emptyState = document.createElement('div');
|
| 155 |
+
emptyState.className = 'empty-state';
|
| 156 |
+
emptyState.innerHTML = `
|
| 157 |
+
<i class="fas fa-comments"></i>
|
| 158 |
+
<p>${language === 'urdu' ? 'ابھی تک کوئی اردو سوالات نہیں ہیں۔' : 'No questions available yet.'}</p>
|
| 159 |
+
`;
|
| 160 |
+
container.appendChild(emptyState);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function createQuestionCard(questionData, language) {
|
| 165 |
+
const questionCard = document.createElement('button');
|
| 166 |
+
questionCard.className = `question-card ${language === 'urdu' ? 'urdu-text' : ''} predefined-card`;
|
| 167 |
+
questionCard.setAttribute('data-question', questionData.question);
|
| 168 |
+
const icon = questionData.icon || 'fas fa-question-circle';
|
| 169 |
+
questionCard.innerHTML = `
|
| 170 |
+
<div class="card-icon ${questionData.category || 'general'}">
|
| 171 |
+
<i class="${icon}"></i>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="card-content">
|
| 174 |
+
<h3>${questionData.question}</h3>
|
| 175 |
+
</div>
|
| 176 |
+
<div class="card-arrow">
|
| 177 |
+
<i class="fas fa-chevron-right"></i>
|
| 178 |
+
</div>
|
| 179 |
+
`;
|
| 180 |
+
questionCard.addEventListener('click', function() {
|
| 181 |
+
userInput.value = questionData.question;
|
| 182 |
+
sendMessage();
|
| 183 |
+
});
|
| 184 |
+
return questionCard;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
async function sendMessage() {
|
| 188 |
+
const message = userInput.value.trim();
|
| 189 |
+
if (!message) return;
|
| 190 |
+
|
| 191 |
+
addMessageToChat(message, 'user', currentLanguage);
|
| 192 |
+
userInput.value = '';
|
| 193 |
+
userInput.disabled = true;
|
| 194 |
+
sendButton.disabled = true;
|
| 195 |
+
showTypingIndicator();
|
| 196 |
+
|
| 197 |
+
try {
|
| 198 |
+
const response = await fetch('/ask-query', {
|
| 199 |
+
method: 'POST',
|
| 200 |
+
headers: { 'Content-Type': 'application/json' },
|
| 201 |
+
body: JSON.stringify({ query: message, language: currentLanguage, response_type: 'text' })
|
| 202 |
+
});
|
| 203 |
+
const data = await response.json();
|
| 204 |
+
hideTypingIndicator();
|
| 205 |
+
|
| 206 |
+
if (data.status === 'success') {
|
| 207 |
+
addMessageToChat(data.answer, 'system', data.language);
|
| 208 |
+
updateMessageCount();
|
| 209 |
+
} else {
|
| 210 |
+
addMessageToChat("I'm having trouble processing your request. Please try again.", 'system', 'english');
|
| 211 |
+
}
|
| 212 |
+
} catch (error) {
|
| 213 |
+
console.error('Error:', error);
|
| 214 |
+
hideTypingIndicator();
|
| 215 |
+
addMessageToChat("Connection issue. Please try again.", 'system', 'english');
|
| 216 |
+
} finally {
|
| 217 |
+
userInput.disabled = false;
|
| 218 |
+
sendButton.disabled = false;
|
| 219 |
+
userInput.focus();
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function cleanUrduText(text) {
|
| 224 |
+
const urduFixes = {
|
| 225 |
+
// Character fixes
|
| 226 |
+
'ہےہ': 'ہے',
|
| 227 |
+
'مہےں': 'میں',
|
| 228 |
+
'ہےں': 'ہیں',
|
| 229 |
+
'ھے': 'ہے',
|
| 230 |
+
'ھوں': 'ہوں',
|
| 231 |
+
'ھیں': 'ہیں',
|
| 232 |
+
'ےے': 'ے',
|
| 233 |
+
'ںں': 'ں',
|
| 234 |
+
'ہہ': 'ہ',
|
| 235 |
+
'یی': 'ی',
|
| 236 |
+
|
| 237 |
+
// Word fixes
|
| 238 |
+
'مجہے': 'مجھے',
|
| 239 |
+
'پروگرہوں': 'پروگرام',
|
| 240 |
+
'کہےنسر': 'کینسر',
|
| 241 |
+
'ڈڈاکٹر': 'ڈاکٹر',
|
| 242 |
+
'کا ے لہےے': 'کے لیے',
|
| 243 |
+
'جسے سے': 'جس سے',
|
| 244 |
+
'اکٹر': 'ڈاکٹر',
|
| 245 |
+
'اکیل': 'اکیلے',
|
| 246 |
+
'میش': 'میں',
|
| 247 |
+
'وتی': 'ہوتی',
|
| 248 |
+
'لکی': 'ہلکی',
|
| 249 |
+
'بتر': 'بہتر',
|
| 250 |
+
|
| 251 |
+
// Grammar fixes
|
| 252 |
+
'ک دوران': 'کے دوران',
|
| 253 |
+
'ک بار': 'کے بارے',
|
| 254 |
+
'ک بعد': 'کے بعد',
|
| 255 |
+
'ک لی': 'کے لیے',
|
| 256 |
+
'ک ساتھ': 'کے ساتھ',
|
| 257 |
+
'ک طور': 'کے طور',
|
| 258 |
+
'ک ذریع': 'کے ذریعے',
|
| 259 |
+
'ک مطابق': 'کے مطابق'
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
let cleanedText = text;
|
| 263 |
+
|
| 264 |
+
// Apply all fixes
|
| 265 |
+
Object.keys(urduFixes).forEach(wrong => {
|
| 266 |
+
const regex = new RegExp(escapeRegExp(wrong), 'g');
|
| 267 |
+
cleanedText = cleanedText.replace(regex, urduFixes[wrong]);
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
// Fix spacing issues
|
| 271 |
+
cleanedText = cleanedText.replace(/\s+/g, ' ');
|
| 272 |
+
cleanedText = cleanedText.replace(/ \./g, '.');
|
| 273 |
+
cleanedText = cleanedText.replace(/ ،/g, '،');
|
| 274 |
+
cleanedText = cleanedText.replace(/ /g, ' ');
|
| 275 |
+
cleanedText = cleanedText.replace(/۔۔/g, '۔');
|
| 276 |
+
|
| 277 |
+
return cleanedText.trim();
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
function escapeRegExp(string) {
|
| 281 |
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
function addMessageToChat(message, sender, language = 'english') {
|
| 285 |
+
// Clean Urdu text before displaying
|
| 286 |
+
if (language === 'urdu') {
|
| 287 |
+
message = cleanUrduText(message);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
const messageDiv = document.createElement('div');
|
| 291 |
+
messageDiv.className = `message ${sender}-message`;
|
| 292 |
+
if (language === 'urdu') {
|
| 293 |
+
messageDiv.classList.add('urdu-text');
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 297 |
+
const avatarIcon = sender === 'user' ? 'fas fa-user' : 'fas fa-robot';
|
| 298 |
+
|
| 299 |
+
messageDiv.innerHTML = `
|
| 300 |
+
<div class="message-avatar">
|
| 301 |
+
<i class="${avatarIcon}"></i>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="message-content ${language === 'urdu' ? 'urdu-text' : ''}">
|
| 304 |
+
<p>${formatMessage(message)}</p>
|
| 305 |
+
<span class="message-time">${timestamp}</span>
|
| 306 |
+
${language === 'urdu' ? '<div class="language-badge">اردو</div>' : ''}
|
| 307 |
+
</div>
|
| 308 |
+
`;
|
| 309 |
+
|
| 310 |
+
chatMessages.appendChild(messageDiv);
|
| 311 |
+
scrollToBottom();
|
| 312 |
+
|
| 313 |
+
// Apply animation to new messages only
|
| 314 |
+
setTimeout(() => {
|
| 315 |
+
messageDiv.style.opacity = '1';
|
| 316 |
+
messageDiv.style.transform = 'translateY(0) scale(1)';
|
| 317 |
+
}, 10);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
function formatMessage(message) {
|
| 321 |
+
return message.replace(/\n/g, '<br>');
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function showTypingIndicator() {
|
| 325 |
+
typingIndicator.style.display = 'flex';
|
| 326 |
+
scrollToBottom();
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
function hideTypingIndicator() {
|
| 330 |
+
typingIndicator.style.display = 'none';
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
function updateMessageCount() {
|
| 334 |
+
messageCounter++;
|
| 335 |
+
messageCount.textContent = messageCounter;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function scrollToBottom() {
|
| 339 |
+
setTimeout(() => {
|
| 340 |
+
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
|
| 341 |
+
}, 100);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Voice Recording Feature - Now returns text responses only
|
| 345 |
+
async function startRecording() {
|
| 346 |
+
try {
|
| 347 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 348 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 349 |
+
audioChunks = [];
|
| 350 |
+
|
| 351 |
+
mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
|
| 352 |
+
mediaRecorder.onstop = async () => {
|
| 353 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 354 |
+
const formData = new FormData();
|
| 355 |
+
formData.append('file', audioBlob, 'voiceNote.webm');
|
| 356 |
+
formData.append('language', currentLanguage); // Pass current tab language
|
| 357 |
+
|
| 358 |
+
// Add user voice message to chat
|
| 359 |
+
addUserVoiceMessageToChat(audioBlob);
|
| 360 |
+
showTypingIndicator();
|
| 361 |
+
|
| 362 |
+
try {
|
| 363 |
+
const response = await fetch('/voice-query', {
|
| 364 |
+
method: 'POST',
|
| 365 |
+
body: formData
|
| 366 |
+
});
|
| 367 |
+
const data = await response.json();
|
| 368 |
+
hideTypingIndicator();
|
| 369 |
+
|
| 370 |
+
if (data.status === 'success') {
|
| 371 |
+
// ✅ ALWAYS show text response for voice queries
|
| 372 |
+
if (data.text && data.text.trim() !== '') {
|
| 373 |
+
addMessageToChat(data.text, 'system', data.language);
|
| 374 |
+
} else {
|
| 375 |
+
// Fallback message
|
| 376 |
+
const fallbackMessage = data.language === 'urdu'
|
| 377 |
+
? "میں آپ کی آواز کا پیغام سمجھ گئی ہوں۔ آپ کیسے مدد کر سکتی ہوں؟"
|
| 378 |
+
: "I've processed your voice message. How can I help you further?";
|
| 379 |
+
addMessageToChat(fallbackMessage, 'system', data.language);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
updateMessageCount();
|
| 383 |
+
} else {
|
| 384 |
+
const errorMessage = currentLanguage === 'urdu'
|
| 385 |
+
? "معذرت، آپ کی آواز کا پیغام پروسیس نہیں کر سکی۔"
|
| 386 |
+
: "Sorry, couldn't process your voice message.";
|
| 387 |
+
addMessageToChat(errorMessage, 'system', currentLanguage);
|
| 388 |
+
}
|
| 389 |
+
} catch (err) {
|
| 390 |
+
console.error('Voice query error:', err);
|
| 391 |
+
hideTypingIndicator();
|
| 392 |
+
const errorMessage = currentLanguage === 'urdu'
|
| 393 |
+
? "آواز کے پروسیس میں خرابی۔"
|
| 394 |
+
: "Error processing voice input.";
|
| 395 |
+
addMessageToChat(errorMessage, 'system', currentLanguage);
|
| 396 |
+
}
|
| 397 |
+
};
|
| 398 |
+
|
| 399 |
+
mediaRecorder.start();
|
| 400 |
+
isRecording = true;
|
| 401 |
+
voiceButton.classList.add('recording');
|
| 402 |
+
voiceButton.innerHTML = '<i class="fas fa-stop"></i>';
|
| 403 |
+
} catch (err) {
|
| 404 |
+
console.error('Microphone access error:', err);
|
| 405 |
+
const errorMessage = currentLanguage === 'urdu'
|
| 406 |
+
? 'براہ کرم آواز ریکارڈ کرنے کے لیے مائیکروفون کی رسائی کی اجازت دیں۔'
|
| 407 |
+
: 'Please allow microphone access to record voice messages.';
|
| 408 |
+
alert(errorMessage);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function addUserVoiceMessageToChat(audioBlob) {
|
| 413 |
+
const messageDiv = document.createElement('div');
|
| 414 |
+
messageDiv.className = `message user-message audio-message`;
|
| 415 |
+
|
| 416 |
+
const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 417 |
+
const audioUrl = URL.createObjectURL(audioBlob);
|
| 418 |
+
|
| 419 |
+
messageDiv.innerHTML = `
|
| 420 |
+
<div class="message-avatar">
|
| 421 |
+
<i class="fas fa-user"></i>
|
| 422 |
+
</div>
|
| 423 |
+
<div class="message-content">
|
| 424 |
+
<div class="audio-message-container user-audio">
|
| 425 |
+
<div class="audio-player-wrapper">
|
| 426 |
+
<audio controls class="voice-note-player">
|
| 427 |
+
<source src="${audioUrl}" type="audio/webm">
|
| 428 |
+
Your browser does not support the audio element.
|
| 429 |
+
</audio>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="audio-duration">Your voice message</div>
|
| 432 |
+
</div>
|
| 433 |
+
<span class="message-time">${timestamp}</span>
|
| 434 |
+
</div>
|
| 435 |
+
`;
|
| 436 |
+
|
| 437 |
+
chatMessages.appendChild(messageDiv);
|
| 438 |
+
scrollToBottom();
|
| 439 |
+
|
| 440 |
+
setTimeout(() => {
|
| 441 |
+
messageDiv.style.opacity = '1';
|
| 442 |
+
messageDiv.style.transform = 'translateY(0) scale(1)';
|
| 443 |
+
}, 10);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function stopRecording() {
|
| 447 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 448 |
+
mediaRecorder.stop();
|
| 449 |
+
}
|
| 450 |
+
isRecording = false;
|
| 451 |
+
voiceButton.classList.remove('recording');
|
| 452 |
+
voiceButton.innerHTML = '<i class="fas fa-microphone"></i>';
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
// Logo animation
|
| 456 |
+
const logo = document.querySelector('.logo');
|
| 457 |
+
if (logo) {
|
| 458 |
+
setInterval(() => {
|
| 459 |
+
logo.style.transform = 'rotate(5deg)';
|
| 460 |
+
setTimeout(() => { logo.style.transform = 'rotate(-5deg)'; }, 1000);
|
| 461 |
+
setTimeout(() => { logo.style.transform = 'rotate(0deg)'; }, 2000);
|
| 462 |
+
}, 5000);
|
| 463 |
+
}
|
styles.css
ADDED
|
@@ -0,0 +1,1344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--primary-pink: #ff6b93;
|
| 3 |
+
--primary-pink-light: #ff8fa9;
|
| 4 |
+
--primary-pink-lighter: #ffb6c8;
|
| 5 |
+
--primary-pink-lightest: #ffe8ee;
|
| 6 |
+
--secondary-pink: #f8c9d4;
|
| 7 |
+
--accent-pink: #ff4d7a;
|
| 8 |
+
--white: #ffffff;
|
| 9 |
+
--white-soft: #fefefe;
|
| 10 |
+
--white-softer: #fafafa;
|
| 11 |
+
--gray-light: #f5f5f5;
|
| 12 |
+
--gray-medium: #e0e0e0;
|
| 13 |
+
--gray-dark: #666666;
|
| 14 |
+
--text-dark: #333333;
|
| 15 |
+
--text-secondary: #666666;
|
| 16 |
+
--shadow-soft: 0 8px 32px rgba(255, 107, 147, 0.1);
|
| 17 |
+
--shadow-medium: 0 15px 35px rgba(255, 107, 147, 0.15);
|
| 18 |
+
--shadow-strong: 0 20px 50px rgba(255, 107, 147, 0.2);
|
| 19 |
+
--border-radius: 24px;
|
| 20 |
+
--border-radius-small: 16px;
|
| 21 |
+
--transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
| 22 |
+
--transition-fast: all 0.2s ease;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
* {
|
| 26 |
+
margin: 0;
|
| 27 |
+
padding: 0;
|
| 28 |
+
box-sizing: border-box;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: 'Poppins', sans-serif;
|
| 33 |
+
background: linear-gradient(135deg, #fff5f7 0%, #ffffff 100%);
|
| 34 |
+
color: var(--text-dark);
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
overflow-x: hidden;
|
| 37 |
+
position: relative;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.floating-elements {
|
| 41 |
+
position: fixed;
|
| 42 |
+
top: 0;
|
| 43 |
+
left: 0;
|
| 44 |
+
width: 100%;
|
| 45 |
+
height: 100%;
|
| 46 |
+
z-index: -1;
|
| 47 |
+
overflow: hidden;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.floating-element {
|
| 51 |
+
position: absolute;
|
| 52 |
+
border-radius: 50%;
|
| 53 |
+
background: linear-gradient(135deg, var(--primary-pink-lightest), var(--primary-pink-lighter));
|
| 54 |
+
opacity: 0.4;
|
| 55 |
+
animation: float 20s infinite linear;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.floating-element.el-1 {
|
| 59 |
+
width: 200px;
|
| 60 |
+
height: 200px;
|
| 61 |
+
top: 10%;
|
| 62 |
+
left: 5%;
|
| 63 |
+
animation-duration: 25s;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.floating-element.el-2 {
|
| 67 |
+
width: 150px;
|
| 68 |
+
height: 150px;
|
| 69 |
+
top: 60%;
|
| 70 |
+
right: 10%;
|
| 71 |
+
animation-duration: 20s;
|
| 72 |
+
animation-direction: reverse;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.floating-element.el-3 {
|
| 76 |
+
width: 100px;
|
| 77 |
+
height: 100px;
|
| 78 |
+
bottom: 20%;
|
| 79 |
+
left: 15%;
|
| 80 |
+
animation-duration: 30s;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.floating-element.el-4 {
|
| 84 |
+
width: 120px;
|
| 85 |
+
height: 120px;
|
| 86 |
+
top: 20%;
|
| 87 |
+
right: 20%;
|
| 88 |
+
animation-duration: 35s;
|
| 89 |
+
animation-direction: reverse;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@keyframes float {
|
| 93 |
+
0% {
|
| 94 |
+
transform: translateY(0) rotate(0deg);
|
| 95 |
+
}
|
| 96 |
+
33% {
|
| 97 |
+
transform: translateY(-30px) rotate(120deg);
|
| 98 |
+
}
|
| 99 |
+
66% {
|
| 100 |
+
transform: translateY(20px) rotate(240deg);
|
| 101 |
+
}
|
| 102 |
+
100% {
|
| 103 |
+
transform: translateY(0) rotate(360deg);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.container {
|
| 108 |
+
max-width: 1400px;
|
| 109 |
+
margin: 0 auto;
|
| 110 |
+
padding: 2rem;
|
| 111 |
+
width: 100%;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.header {
|
| 115 |
+
text-align: center;
|
| 116 |
+
margin-bottom: 3rem;
|
| 117 |
+
animation: fadeInUp 1s ease-out;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.header-content {
|
| 121 |
+
max-width: 800px;
|
| 122 |
+
margin: 0 auto;
|
| 123 |
+
width: 100%;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.logo-container {
|
| 127 |
+
display: flex;
|
| 128 |
+
align-items: center;
|
| 129 |
+
justify-content: center;
|
| 130 |
+
gap: 1rem;
|
| 131 |
+
margin-bottom: 1rem;
|
| 132 |
+
flex-wrap: wrap;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.logo {
|
| 136 |
+
width: 70px;
|
| 137 |
+
height: 70px;
|
| 138 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 139 |
+
border-radius: 50%;
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
font-size: 2rem;
|
| 144 |
+
color: var(--white);
|
| 145 |
+
box-shadow: var(--shadow-strong);
|
| 146 |
+
animation: pulse 2s infinite;
|
| 147 |
+
flex-shrink: 0;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
@keyframes pulse {
|
| 151 |
+
0% {
|
| 152 |
+
transform: scale(1);
|
| 153 |
+
box-shadow: var(--shadow-strong);
|
| 154 |
+
}
|
| 155 |
+
50% {
|
| 156 |
+
transform: scale(1.05);
|
| 157 |
+
box-shadow: 0 0 30px rgba(255, 107, 147, 0.4);
|
| 158 |
+
}
|
| 159 |
+
100% {
|
| 160 |
+
transform: scale(1);
|
| 161 |
+
box-shadow: var(--shadow-strong);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.title {
|
| 166 |
+
font-family: 'Playfair Display', serif;
|
| 167 |
+
font-size: clamp(2.5rem, 5vw, 3.5rem);
|
| 168 |
+
font-weight: 600;
|
| 169 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 170 |
+
-webkit-background-clip: text;
|
| 171 |
+
-webkit-text-fill-color: transparent;
|
| 172 |
+
background-clip: text;
|
| 173 |
+
margin-bottom: 0.5rem;
|
| 174 |
+
line-height: 1.2;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.subtitle {
|
| 178 |
+
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
|
| 179 |
+
color: var(--text-secondary);
|
| 180 |
+
margin-bottom: 1rem;
|
| 181 |
+
font-weight: 400;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.compassion-message {
|
| 185 |
+
font-size: clamp(0.9rem, 2vw, 1.1rem);
|
| 186 |
+
color: var(--text-secondary);
|
| 187 |
+
line-height: 1.6;
|
| 188 |
+
max-width: 600px;
|
| 189 |
+
margin: 0 auto;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.main-content {
|
| 193 |
+
display: grid;
|
| 194 |
+
grid-template-columns: 1fr 1.5fr;
|
| 195 |
+
gap: 2.5rem;
|
| 196 |
+
animation: fadeInUp 1s ease-out 0.3s both;
|
| 197 |
+
width: 100%;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
@keyframes fadeInUp {
|
| 201 |
+
from {
|
| 202 |
+
opacity: 0;
|
| 203 |
+
transform: translateY(30px);
|
| 204 |
+
}
|
| 205 |
+
to {
|
| 206 |
+
opacity: 1;
|
| 207 |
+
transform: translateY(0);
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.quick-questions {
|
| 212 |
+
animation: slideInLeft 1s ease-out 0.5s both;
|
| 213 |
+
width: 100%;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
@keyframes slideInLeft {
|
| 217 |
+
from {
|
| 218 |
+
opacity: 0;
|
| 219 |
+
transform: translateX(-30px);
|
| 220 |
+
}
|
| 221 |
+
to {
|
| 222 |
+
opacity: 1;
|
| 223 |
+
transform: translateX(0);
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.section-header {
|
| 228 |
+
margin-bottom: 2rem;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.section-title {
|
| 232 |
+
font-family: 'Playfair Display', serif;
|
| 233 |
+
font-size: clamp(1.5rem, 3vw, 1.8rem);
|
| 234 |
+
font-weight: 600;
|
| 235 |
+
color: var(--text-dark);
|
| 236 |
+
margin-bottom: 0.5rem;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.decoration-line {
|
| 240 |
+
width: 60px;
|
| 241 |
+
height: 4px;
|
| 242 |
+
background: linear-gradient(to right, var(--primary-pink), var(--accent-pink));
|
| 243 |
+
border-radius: 2px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.tabs-container {
|
| 247 |
+
margin-top: 1rem;
|
| 248 |
+
width: 100%;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.tabs {
|
| 252 |
+
display: flex;
|
| 253 |
+
background: var(--white);
|
| 254 |
+
border-radius: 12px;
|
| 255 |
+
padding: 0.5rem;
|
| 256 |
+
margin-bottom: 1.5rem;
|
| 257 |
+
box-shadow: var(--shadow-soft);
|
| 258 |
+
width: 100%;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.tab-btn {
|
| 262 |
+
flex: 1;
|
| 263 |
+
padding: 0.75rem 1rem;
|
| 264 |
+
border: none;
|
| 265 |
+
background: transparent;
|
| 266 |
+
color: var(--text-secondary);
|
| 267 |
+
border-radius: 8px;
|
| 268 |
+
cursor: pointer;
|
| 269 |
+
transition: var(--transition);
|
| 270 |
+
font-weight: 500;
|
| 271 |
+
font-size: clamp(0.8rem, 1.5vw, 0.9rem);
|
| 272 |
+
min-width: 0;
|
| 273 |
+
white-space: nowrap;
|
| 274 |
+
overflow: hidden;
|
| 275 |
+
text-overflow: ellipsis;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.tab-btn.active {
|
| 279 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 280 |
+
color: var(--white);
|
| 281 |
+
box-shadow: var(--shadow-soft);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.tab-btn:hover:not(.active) {
|
| 285 |
+
background: var(--primary-pink-lightest);
|
| 286 |
+
color: var(--primary-pink);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.tab-content {
|
| 290 |
+
display: none;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.tab-content.active {
|
| 294 |
+
display: block;
|
| 295 |
+
animation: fadeIn 0.5s ease-in-out;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
@keyframes fadeIn {
|
| 299 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 300 |
+
to { opacity: 1; transform: translateY(0); }
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.questions-grid {
|
| 304 |
+
display: flex;
|
| 305 |
+
flex-direction: column;
|
| 306 |
+
gap: 1rem;
|
| 307 |
+
width: 100%;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.question-card {
|
| 311 |
+
background: var(--white);
|
| 312 |
+
border: none;
|
| 313 |
+
border-radius: var(--border-radius-small);
|
| 314 |
+
padding: 1.5rem;
|
| 315 |
+
display: flex;
|
| 316 |
+
align-items: center;
|
| 317 |
+
gap: 1rem;
|
| 318 |
+
cursor: pointer;
|
| 319 |
+
transition: var(--transition);
|
| 320 |
+
box-shadow: var(--shadow-soft);
|
| 321 |
+
text-align: left;
|
| 322 |
+
position: relative;
|
| 323 |
+
overflow: hidden;
|
| 324 |
+
width: 100%;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.question-card::before {
|
| 328 |
+
content: '';
|
| 329 |
+
position: absolute;
|
| 330 |
+
top: 0;
|
| 331 |
+
left: -100%;
|
| 332 |
+
width: 100%;
|
| 333 |
+
height: 100%;
|
| 334 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
| 335 |
+
transition: var(--transition);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.question-card:hover::before {
|
| 339 |
+
left: 100%;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.question-card:hover {
|
| 343 |
+
transform: translateY(-5px);
|
| 344 |
+
box-shadow: var(--shadow-medium);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.card-icon {
|
| 348 |
+
width: 50px;
|
| 349 |
+
height: 50px;
|
| 350 |
+
background: linear-gradient(135deg, var(--primary-pink-lightest), var(--secondary-pink));
|
| 351 |
+
border-radius: 12px;
|
| 352 |
+
display: flex;
|
| 353 |
+
align-items: center;
|
| 354 |
+
justify-content: center;
|
| 355 |
+
font-size: 1.2rem;
|
| 356 |
+
color: var(--primary-pink);
|
| 357 |
+
flex-shrink: 0;
|
| 358 |
+
transition: var(--transition);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.question-card:hover .card-icon {
|
| 362 |
+
transform: scale(1.1);
|
| 363 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 364 |
+
color: var(--white);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.card-content {
|
| 368 |
+
flex: 1;
|
| 369 |
+
min-width: 0;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.card-content h3 {
|
| 373 |
+
font-size: clamp(0.9rem, 1.5vw, 1rem);
|
| 374 |
+
font-weight: 600;
|
| 375 |
+
color: var(--text-dark);
|
| 376 |
+
margin-bottom: 0.25rem;
|
| 377 |
+
line-height: 1.4;
|
| 378 |
+
word-wrap: break-word;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.card-arrow {
|
| 382 |
+
color: var(--gray-medium);
|
| 383 |
+
transition: var(--transition);
|
| 384 |
+
flex-shrink: 0;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.question-card:hover .card-arrow {
|
| 388 |
+
color: var(--primary-pink);
|
| 389 |
+
transform: translateX(5px);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.predefined-card {
|
| 393 |
+
border-left: 4px solid var(--primary-pink);
|
| 394 |
+
background: linear-gradient(135deg, var(--white), var(--white-soft));
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.card-icon.symptoms {
|
| 398 |
+
background: linear-gradient(135deg, #4CAF50, #45a049) !important;
|
| 399 |
+
color: white !important;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.card-icon.pain {
|
| 403 |
+
background: linear-gradient(135deg, #FF6B6B, #FF5252) !important;
|
| 404 |
+
color: white !important;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.card-icon.emotional {
|
| 408 |
+
background: linear-gradient(135deg, #FFD93D, #FFCD38) !important;
|
| 409 |
+
color: white !important;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.card-icon.exercise {
|
| 413 |
+
background: linear-gradient(135deg, #6BCF7F, #4CAF50) !important;
|
| 414 |
+
color: white !important;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.card-icon.nutrition {
|
| 418 |
+
background: linear-gradient(135deg, #A78BFA, #8B5CF6) !important;
|
| 419 |
+
color: white !important;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.card-icon.general {
|
| 423 |
+
background: linear-gradient(135deg, #4FC3F7, #29B6F6) !important;
|
| 424 |
+
color: white !important;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/* ===== OPTIMIZED URDU LANGUAGE STYLING - MATCHING ENGLISH SIZE ===== */
|
| 428 |
+
/* ===== OPTIMIZED URDU LANGUAGE STYLING - MATCHING ENGLISH SIZE ===== */
|
| 429 |
+
.urdu-text {
|
| 430 |
+
font-family: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', 'Scheherazade New', 'Lateef', 'Amiri', serif !important;
|
| 431 |
+
direction: rtl !important;
|
| 432 |
+
text-align: right !important;
|
| 433 |
+
line-height: 1.8 !important;
|
| 434 |
+
font-size: 1em !important;
|
| 435 |
+
font-weight: 400 !important;
|
| 436 |
+
letter-spacing: 0 !important;
|
| 437 |
+
word-spacing: normal !important;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
/* Urdu Chat Messages */
|
| 441 |
+
.message.urdu-text .message-content p {
|
| 442 |
+
font-family: 'Noto Nastaliq Urdu', 'Scheherazade New', 'Lateef', serif !important;
|
| 443 |
+
font-size: 1em !important;
|
| 444 |
+
line-height: 2.0 !important;
|
| 445 |
+
text-align: right !important;
|
| 446 |
+
direction: rtl !important;
|
| 447 |
+
margin: 0 !important;
|
| 448 |
+
padding: 0 !important;
|
| 449 |
+
font-feature-settings: "kern" 1, "liga" 1, "clig" 1, "calt" 1;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Urdu Input Field */
|
| 453 |
+
#userInput[style*="direction: rtl"],
|
| 454 |
+
#userInput[style*="text-align: right"] {
|
| 455 |
+
font-family: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', 'Scheherazade New', 'Segoe UI', Tahoma, sans-serif !important;
|
| 456 |
+
font-size: 1em !important;
|
| 457 |
+
direction: rtl !important;
|
| 458 |
+
text-align: right !important;
|
| 459 |
+
line-height: 1.8 !important;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/* Urdu Questions Section */
|
| 463 |
+
#urdu-tab .questions-grid,
|
| 464 |
+
.urdu-questions .question-card {
|
| 465 |
+
font-family: 'Noto Nastaliq Urdu', 'Scheherazade New', 'Lateef', Tahoma, sans-serif !important;
|
| 466 |
+
font-size: 1em !important;
|
| 467 |
+
line-height: 1.8 !important;
|
| 468 |
+
direction: rtl !important;
|
| 469 |
+
text-align: right !important;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* Urdu Empty States */
|
| 473 |
+
#urdu-tab .empty-state {
|
| 474 |
+
font-family: 'Noto Nastaliq Urdu', 'Scheherazade New', 'Lateef', Tahoma, sans-serif !important;
|
| 475 |
+
direction: rtl !important;
|
| 476 |
+
font-size: 1em !important;
|
| 477 |
+
line-height: 1.8 !important;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
/* Urdu Language Badge */
|
| 481 |
+
.language-badge {
|
| 482 |
+
font-family: 'Noto Naskh Arabic', 'Segoe UI', Tahoma, sans-serif !important;
|
| 483 |
+
font-size: 0.75em !important;
|
| 484 |
+
font-weight: 600 !important;
|
| 485 |
+
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
| 486 |
+
color: white !important;
|
| 487 |
+
padding: 4px 10px !important;
|
| 488 |
+
border-radius: 12px !important;
|
| 489 |
+
margin-top: 8px !important;
|
| 490 |
+
display: inline-block !important;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/* Enhanced Text Rendering for Urdu */
|
| 494 |
+
.urdu-text,
|
| 495 |
+
[lang="ur"],
|
| 496 |
+
[dir="rtl"] {
|
| 497 |
+
text-rendering: optimizeLegibility !important;
|
| 498 |
+
-webkit-font-smoothing: antialiased !important;
|
| 499 |
+
-moz-osx-font-smoothing: grayscale !important;
|
| 500 |
+
font-feature-settings: "kern" 1, "liga" 1, "clig" 1, "calt" 1;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
/* Urdu Tab Button */
|
| 504 |
+
.tab-btn[data-tab="urdu"] {
|
| 505 |
+
font-family: 'Noto Naskh Arabic', 'Segoe UI', Tahoma, sans-serif !important;
|
| 506 |
+
font-weight: 600 !important;
|
| 507 |
+
font-size: 0.9em !important;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Urdu Text Card Specific */
|
| 511 |
+
.urdu-text .card-content {
|
| 512 |
+
text-align: right !important;
|
| 513 |
+
direction: rtl !important;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.urdu-text.question-card {
|
| 517 |
+
border-left: none !important;
|
| 518 |
+
border-right: 4px solid var(--primary-pink) !important;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.urdu-text.question-card:hover {
|
| 522 |
+
transform: translateX(-5px) translateY(-5px) !important;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.chat-section {
|
| 526 |
+
animation: slideInRight 1s ease-out 0.5s both;
|
| 527 |
+
width: 100%;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
@keyframes slideInRight {
|
| 531 |
+
from {
|
| 532 |
+
opacity: 0;
|
| 533 |
+
transform: translateX(30px);
|
| 534 |
+
}
|
| 535 |
+
to {
|
| 536 |
+
opacity: 1;
|
| 537 |
+
transform: translateX(0);
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.chat-container {
|
| 542 |
+
background: var(--white);
|
| 543 |
+
border-radius: var(--border-radius);
|
| 544 |
+
box-shadow: var(--shadow-strong);
|
| 545 |
+
overflow: hidden;
|
| 546 |
+
height: 700px;
|
| 547 |
+
display: flex;
|
| 548 |
+
flex-direction: column;
|
| 549 |
+
position: relative;
|
| 550 |
+
width: 100%;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.chat-header {
|
| 554 |
+
background: linear-gradient(135deg, var(--white-soft), var(--white-softer));
|
| 555 |
+
padding: 1.5rem 2rem;
|
| 556 |
+
display: flex;
|
| 557 |
+
justify-content: space-between;
|
| 558 |
+
align-items: center;
|
| 559 |
+
border-bottom: 1px solid var(--gray-light);
|
| 560 |
+
flex-wrap: wrap;
|
| 561 |
+
gap: 1rem;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.chat-profile {
|
| 565 |
+
display: flex;
|
| 566 |
+
align-items: center;
|
| 567 |
+
gap: 1rem;
|
| 568 |
+
flex: 1;
|
| 569 |
+
min-width: 0;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.profile-avatar {
|
| 573 |
+
position: relative;
|
| 574 |
+
width: 60px;
|
| 575 |
+
height: 60px;
|
| 576 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 577 |
+
border-radius: 50%;
|
| 578 |
+
display: flex;
|
| 579 |
+
align-items: center;
|
| 580 |
+
justify-content: center;
|
| 581 |
+
font-size: 1.5rem;
|
| 582 |
+
color: var(--white);
|
| 583 |
+
box-shadow: var(--shadow-soft);
|
| 584 |
+
flex-shrink: 0;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.online-indicator {
|
| 588 |
+
position: absolute;
|
| 589 |
+
bottom: 4px;
|
| 590 |
+
right: 4px;
|
| 591 |
+
width: 14px;
|
| 592 |
+
height: 14px;
|
| 593 |
+
background: #4CAF50;
|
| 594 |
+
border: 2px solid var(--white);
|
| 595 |
+
border-radius: 50%;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.profile-info {
|
| 599 |
+
min-width: 0;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.profile-info h3 {
|
| 603 |
+
font-size: clamp(1.1rem, 2vw, 1.3rem);
|
| 604 |
+
font-weight: 600;
|
| 605 |
+
color: var(--text-dark);
|
| 606 |
+
white-space: nowrap;
|
| 607 |
+
overflow: hidden;
|
| 608 |
+
text-overflow: ellipsis;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.profile-info p {
|
| 612 |
+
color: var(--text-secondary);
|
| 613 |
+
font-size: clamp(0.8rem, 1.5vw, 0.9rem);
|
| 614 |
+
white-space: nowrap;
|
| 615 |
+
overflow: hidden;
|
| 616 |
+
text-overflow: ellipsis;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
.chat-stats {
|
| 620 |
+
display: flex;
|
| 621 |
+
gap: 1.5rem;
|
| 622 |
+
flex-shrink: 0;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.stat {
|
| 626 |
+
text-align: center;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.stat-number {
|
| 630 |
+
display: block;
|
| 631 |
+
font-size: clamp(1rem, 1.5vw, 1.1rem);
|
| 632 |
+
font-weight: 600;
|
| 633 |
+
color: var(--primary-pink);
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.stat-label {
|
| 637 |
+
font-size: clamp(0.7rem, 1.2vw, 0.8rem);
|
| 638 |
+
color: var(--text-secondary);
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
.chat-messages {
|
| 642 |
+
flex: 1;
|
| 643 |
+
overflow-y: auto;
|
| 644 |
+
padding: 2rem;
|
| 645 |
+
display: flex;
|
| 646 |
+
flex-direction: column;
|
| 647 |
+
gap: 1.5rem;
|
| 648 |
+
background: var(--white-softer);
|
| 649 |
+
width: 100%;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.message {
|
| 653 |
+
display: flex;
|
| 654 |
+
gap: 1rem;
|
| 655 |
+
max-width: 85%;
|
| 656 |
+
animation: messageAppear 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 657 |
+
width: fit-content;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
@keyframes messageAppear {
|
| 661 |
+
from {
|
| 662 |
+
opacity: 0;
|
| 663 |
+
transform: translateY(20px) scale(0.95);
|
| 664 |
+
}
|
| 665 |
+
to {
|
| 666 |
+
opacity: 1;
|
| 667 |
+
transform: translateY(0) scale(1);
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
.user-message {
|
| 672 |
+
align-self: flex-end;
|
| 673 |
+
flex-direction: row-reverse;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.system-message {
|
| 677 |
+
align-self: flex-start;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.message-avatar {
|
| 681 |
+
width: 45px;
|
| 682 |
+
height: 45px;
|
| 683 |
+
border-radius: 50%;
|
| 684 |
+
display: flex;
|
| 685 |
+
align-items: center;
|
| 686 |
+
justify-content: center;
|
| 687 |
+
font-size: 1.1rem;
|
| 688 |
+
flex-shrink: 0;
|
| 689 |
+
margin-top: 0.5rem;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
.user-message .message-avatar {
|
| 693 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 694 |
+
color: var(--white);
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.system-message .message-avatar {
|
| 698 |
+
background: linear-gradient(135deg, var(--primary-pink-lightest), var(--secondary-pink));
|
| 699 |
+
color: var(--primary-pink);
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.message-content {
|
| 703 |
+
background: var(--white);
|
| 704 |
+
padding: 1.2rem 1.5rem;
|
| 705 |
+
border-radius: 20px;
|
| 706 |
+
box-shadow: var(--shadow-soft);
|
| 707 |
+
position: relative;
|
| 708 |
+
transition: var(--transition-fast);
|
| 709 |
+
max-width: 100%;
|
| 710 |
+
word-wrap: break-word;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.user-message .message-content {
|
| 714 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 715 |
+
color: var(--white);
|
| 716 |
+
border-bottom-right-radius: 5px;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.system-message .message-content {
|
| 720 |
+
background: var(--white);
|
| 721 |
+
color: var(--text-dark);
|
| 722 |
+
border-bottom-left-radius: 5px;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.message-content::before {
|
| 726 |
+
content: '';
|
| 727 |
+
position: absolute;
|
| 728 |
+
bottom: 0;
|
| 729 |
+
width: 15px;
|
| 730 |
+
height: 15px;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.user-message .message-content::before {
|
| 734 |
+
right: -8px;
|
| 735 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 736 |
+
clip-path: polygon(0 0, 100% 0, 100% 100%);
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.system-message .message-content::before {
|
| 740 |
+
left: -8px;
|
| 741 |
+
background: var(--white);
|
| 742 |
+
clip-path: polygon(0 0, 100% 0, 0 100%);
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
.message-content p {
|
| 746 |
+
line-height: 1.6;
|
| 747 |
+
margin-bottom: 0.5rem;
|
| 748 |
+
word-wrap: break-word;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.message-time {
|
| 752 |
+
font-size: 0.75rem;
|
| 753 |
+
opacity: 0.7;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.typing-indicator {
|
| 757 |
+
display: none;
|
| 758 |
+
align-items: center;
|
| 759 |
+
gap: 1rem;
|
| 760 |
+
padding: 1rem 2rem;
|
| 761 |
+
background: var(--white-softer);
|
| 762 |
+
border-top: 1px solid var(--gray-light);
|
| 763 |
+
width: 100%;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.typing-dots {
|
| 767 |
+
display: flex;
|
| 768 |
+
gap: 4px;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.typing-dots span {
|
| 772 |
+
width: 8px;
|
| 773 |
+
height: 8px;
|
| 774 |
+
background: var(--primary-pink);
|
| 775 |
+
border-radius: 50%;
|
| 776 |
+
animation: typing 1.4s infinite ease-in-out;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
|
| 780 |
+
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
|
| 781 |
+
|
| 782 |
+
@keyframes typing {
|
| 783 |
+
0%, 80%, 100% {
|
| 784 |
+
transform: scale(0.8);
|
| 785 |
+
opacity: 0.5;
|
| 786 |
+
}
|
| 787 |
+
40% {
|
| 788 |
+
transform: scale(1);
|
| 789 |
+
opacity: 1;
|
| 790 |
+
}
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.typing-indicator p {
|
| 794 |
+
color: var(--text-secondary);
|
| 795 |
+
font-size: clamp(0.8rem, 1.5vw, 0.9rem);
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.chat-input-container {
|
| 799 |
+
padding: 1.5rem 2rem;
|
| 800 |
+
border-top: 1px solid var(--gray-light);
|
| 801 |
+
background: var(--white);
|
| 802 |
+
width: 100%;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.input-wrapper {
|
| 806 |
+
display: flex;
|
| 807 |
+
gap: 1rem;
|
| 808 |
+
margin-bottom: 0.5rem;
|
| 809 |
+
width: 100%;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
#userInput {
|
| 813 |
+
flex: 1;
|
| 814 |
+
border: none;
|
| 815 |
+
outline: none;
|
| 816 |
+
padding: 1.2rem 1.5rem;
|
| 817 |
+
font-size: clamp(0.9rem, 1.5vw, 1rem);
|
| 818 |
+
background: var(--white-softer);
|
| 819 |
+
border-radius: 50px;
|
| 820 |
+
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 821 |
+
transition: var(--transition);
|
| 822 |
+
border: 2px solid transparent;
|
| 823 |
+
min-width: 0;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
#userInput:focus {
|
| 827 |
+
border-color: var(--primary-pink-light);
|
| 828 |
+
box-shadow: 0 0 0 3px rgba(255, 107, 147, 0.1), inset 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
.send-button {
|
| 832 |
+
display: flex;
|
| 833 |
+
align-items: center;
|
| 834 |
+
gap: 0.5rem;
|
| 835 |
+
padding: 1.2rem 1.8rem;
|
| 836 |
+
background: linear-gradient(135deg, var(--primary-pink), var(--accent-pink));
|
| 837 |
+
color: var(--white);
|
| 838 |
+
border: none;
|
| 839 |
+
border-radius: 50px;
|
| 840 |
+
cursor: pointer;
|
| 841 |
+
transition: var(--transition);
|
| 842 |
+
font-weight: 500;
|
| 843 |
+
box-shadow: var(--shadow-soft);
|
| 844 |
+
flex-shrink: 0;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.send-button:hover {
|
| 848 |
+
transform: translateY(-2px);
|
| 849 |
+
box-shadow: var(--shadow-medium);
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.send-button:active {
|
| 853 |
+
transform: translateY(0);
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
.voice-button {
|
| 857 |
+
display: flex;
|
| 858 |
+
align-items: center;
|
| 859 |
+
justify-content: center;
|
| 860 |
+
padding: 1.2rem 1.4rem;
|
| 861 |
+
background: linear-gradient(135deg, var(--primary-pink-lightest), var(--secondary-pink));
|
| 862 |
+
color: var(--primary-pink);
|
| 863 |
+
border: none;
|
| 864 |
+
border-radius: 50%;
|
| 865 |
+
cursor: pointer;
|
| 866 |
+
transition: var(--transition);
|
| 867 |
+
box-shadow: var(--shadow-soft);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.voice-button.recording {
|
| 871 |
+
background: linear-gradient(135deg, #ff4d7a, #ff6b93);
|
| 872 |
+
color: white;
|
| 873 |
+
animation: pulse 1s infinite;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.voice-button:hover {
|
| 877 |
+
transform: scale(1.1);
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
.input-hint {
|
| 881 |
+
display: flex;
|
| 882 |
+
align-items: center;
|
| 883 |
+
gap: 0.5rem;
|
| 884 |
+
font-size: clamp(0.7rem, 1.2vw, 0.8rem);
|
| 885 |
+
color: var(--text-secondary);
|
| 886 |
+
padding-left: 1rem;
|
| 887 |
+
flex-wrap: wrap;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.input-hint i {
|
| 891 |
+
color: var(--primary-pink);
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
.chat-messages::-webkit-scrollbar {
|
| 895 |
+
width: 6px;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.chat-messages::-webkit-scrollbar-track {
|
| 899 |
+
background: rgba(0, 0, 0, 0.05);
|
| 900 |
+
border-radius: 3px;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
.chat-messages::-webkit-scrollbar-thumb {
|
| 904 |
+
background: var(--primary-pink-light);
|
| 905 |
+
border-radius: 3px;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.chat-messages::-webkit-scrollbar-thumb:hover {
|
| 909 |
+
background: var(--primary-pink);
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.empty-state {
|
| 913 |
+
text-align: center;
|
| 914 |
+
padding: 3rem 2rem;
|
| 915 |
+
color: var(--text-secondary);
|
| 916 |
+
width: 100%;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.empty-state i {
|
| 920 |
+
font-size: clamp(2.5rem, 5vw, 3rem);
|
| 921 |
+
margin-bottom: 1rem;
|
| 922 |
+
color: var(--primary-pink-light);
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.empty-state p {
|
| 926 |
+
font-size: clamp(0.9rem, 1.5vw, 1rem);
|
| 927 |
+
line-height: 1.5;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
#activeLanguage {
|
| 931 |
+
color: var(--primary-pink);
|
| 932 |
+
font-weight: 600;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
#currentLanguageDisplay {
|
| 936 |
+
color: var(--primary-pink);
|
| 937 |
+
font-weight: 600;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.audio-message {
|
| 941 |
+
margin: 10px 0;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.audio-message-container {
|
| 945 |
+
background: #f8f9fa;
|
| 946 |
+
border-radius: 18px;
|
| 947 |
+
padding: 12px 16px;
|
| 948 |
+
max-width: 280px;
|
| 949 |
+
border: 1px solid #e9ecef;
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
.user-audio .audio-message-container {
|
| 953 |
+
background: #007bff;
|
| 954 |
+
color: white;
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
.audio-player-wrapper {
|
| 958 |
+
display: flex;
|
| 959 |
+
align-items: center;
|
| 960 |
+
gap: 12px;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
.voice-note-player {
|
| 964 |
+
flex: 1;
|
| 965 |
+
height: 32px;
|
| 966 |
+
border-radius: 16px;
|
| 967 |
+
background: white;
|
| 968 |
+
border: 1px solid #ddd;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.user-audio .voice-note-player {
|
| 972 |
+
background: #0056b3;
|
| 973 |
+
border-color: #004085;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
.audio-visualizer {
|
| 977 |
+
display: flex;
|
| 978 |
+
align-items: center;
|
| 979 |
+
gap: 2px;
|
| 980 |
+
height: 20px;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.audio-wave {
|
| 984 |
+
width: 3px;
|
| 985 |
+
height: 8px;
|
| 986 |
+
background: #6c757d;
|
| 987 |
+
border-radius: 2px;
|
| 988 |
+
transition: all 0.3s ease;
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
.audio-visualizer.playing .audio-wave {
|
| 992 |
+
background: #007bff;
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
.user-audio .audio-wave {
|
| 996 |
+
background: rgba(255, 255, 255, 0.6);
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
.user-audio .audio-visualizer.playing .audio-wave {
|
| 1000 |
+
background: white;
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
.audio-duration {
|
| 1004 |
+
font-size: 11px;
|
| 1005 |
+
color: #6c757d;
|
| 1006 |
+
margin-top: 4px;
|
| 1007 |
+
text-align: center;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
.user-audio .audio-duration {
|
| 1011 |
+
color: rgba(255, 255, 255, 0.8);
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
@keyframes pulse {
|
| 1015 |
+
0% {
|
| 1016 |
+
height: 8px;
|
| 1017 |
+
opacity: 0.7;
|
| 1018 |
+
}
|
| 1019 |
+
100% {
|
| 1020 |
+
height: 16px;
|
| 1021 |
+
opacity: 1;
|
| 1022 |
+
}
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.manual-play-required .audio-player-wrapper::before {
|
| 1026 |
+
content: "Click to play";
|
| 1027 |
+
position: absolute;
|
| 1028 |
+
top: -20px;
|
| 1029 |
+
left: 50%;
|
| 1030 |
+
transform: translateX(-50%);
|
| 1031 |
+
background: #ffc107;
|
| 1032 |
+
color: #212529;
|
| 1033 |
+
padding: 4px 8px;
|
| 1034 |
+
border-radius: 4px;
|
| 1035 |
+
font-size: 10px;
|
| 1036 |
+
white-space: nowrap;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.message {
|
| 1040 |
+
opacity: 0;
|
| 1041 |
+
transform: translateY(20px) scale(0.95);
|
| 1042 |
+
transition: all 0.3s ease;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.message.show {
|
| 1046 |
+
opacity: 1;
|
| 1047 |
+
transform: translateY(0) scale(1);
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
#voiceButton.recording {
|
| 1051 |
+
background: #dc3545;
|
| 1052 |
+
animation: pulse 1.5s infinite;
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
@keyframes pulse {
|
| 1056 |
+
0% {
|
| 1057 |
+
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7);
|
| 1058 |
+
}
|
| 1059 |
+
70% {
|
| 1060 |
+
box-shadow: 0 0 0 10px rgba(220, 53, 69, 0);
|
| 1061 |
+
}
|
| 1062 |
+
100% {
|
| 1063 |
+
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
|
| 1064 |
+
}
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
@media (max-width: 768px) {
|
| 1068 |
+
.urdu-text {
|
| 1069 |
+
font-size: 0.95em;
|
| 1070 |
+
line-height: 1.7;
|
| 1071 |
+
}
|
| 1072 |
+
|
| 1073 |
+
.chat-messages .message.urdu-text .message-content p {
|
| 1074 |
+
font-size: 0.95em;
|
| 1075 |
+
line-height: 1.9;
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
.audio-message-container {
|
| 1079 |
+
max-width: 220px;
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
.voice-note-player {
|
| 1083 |
+
height: 28px;
|
| 1084 |
+
}
|
| 1085 |
+
}
|
| 1086 |
+
@media (max-width: 1200px) {
|
| 1087 |
+
.container {
|
| 1088 |
+
padding: 1.5rem;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
.main-content {
|
| 1092 |
+
gap: 2rem;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
.chat-container {
|
| 1096 |
+
height: 650px;
|
| 1097 |
+
}
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
@media (max-width: 1024px) {
|
| 1101 |
+
.main-content {
|
| 1102 |
+
grid-template-columns: 1fr;
|
| 1103 |
+
gap: 2rem;
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
+
.chat-container {
|
| 1107 |
+
height: 600px;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
.chat-header {
|
| 1111 |
+
padding: 1.25rem;
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
.chat-messages {
|
| 1115 |
+
padding: 1.5rem;
|
| 1116 |
+
}
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
@media (max-width: 768px) {
|
| 1120 |
+
.container {
|
| 1121 |
+
padding: 1rem;
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
.logo-container {
|
| 1125 |
+
flex-direction: column;
|
| 1126 |
+
text-align: center;
|
| 1127 |
+
gap: 0.5rem;
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
.title {
|
| 1131 |
+
font-size: 2.5rem;
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
.chat-header {
|
| 1135 |
+
padding: 1rem;
|
| 1136 |
+
flex-direction: column;
|
| 1137 |
+
gap: 1rem;
|
| 1138 |
+
align-items: flex-start;
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
.chat-profile {
|
| 1142 |
+
width: 100%;
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
.chat-stats {
|
| 1146 |
+
align-self: stretch;
|
| 1147 |
+
justify-content: space-around;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
.message {
|
| 1151 |
+
max-width: 95%;
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
.chat-messages {
|
| 1155 |
+
padding: 1rem;
|
| 1156 |
+
gap: 1rem;
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
.chat-input-container {
|
| 1160 |
+
padding: 1rem;
|
| 1161 |
+
}
|
| 1162 |
+
|
| 1163 |
+
.input-wrapper {
|
| 1164 |
+
flex-direction: column;
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
.send-button {
|
| 1168 |
+
align-self: flex-end;
|
| 1169 |
+
width: fit-content;
|
| 1170 |
+
padding: 1rem 1.5rem;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
.tabs {
|
| 1174 |
+
flex-direction: column;
|
| 1175 |
+
gap: 0.5rem;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.tab-btn {
|
| 1179 |
+
padding: 1rem;
|
| 1180 |
+
text-align: center;
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
.question-card {
|
| 1184 |
+
padding: 1.25rem;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
.card-icon {
|
| 1188 |
+
width: 45px;
|
| 1189 |
+
height: 45px;
|
| 1190 |
+
font-size: 1.1rem;
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
.empty-state {
|
| 1194 |
+
padding: 2rem 1rem;
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
.profile-avatar {
|
| 1198 |
+
width: 50px;
|
| 1199 |
+
height: 50px;
|
| 1200 |
+
font-size: 1.3rem;
|
| 1201 |
+
}
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
@media (max-width: 480px) {
|
| 1205 |
+
.container {
|
| 1206 |
+
padding: 0.75rem;
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.header {
|
| 1210 |
+
margin-bottom: 2rem;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
.logo {
|
| 1214 |
+
width: 60px;
|
| 1215 |
+
height: 60px;
|
| 1216 |
+
font-size: 1.7rem;
|
| 1217 |
+
}
|
| 1218 |
+
|
| 1219 |
+
.title {
|
| 1220 |
+
font-size: 2rem;
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
.subtitle {
|
| 1224 |
+
font-size: 1rem;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
.compassion-message {
|
| 1228 |
+
font-size: 0.85rem;
|
| 1229 |
+
}
|
| 1230 |
+
|
| 1231 |
+
.main-content {
|
| 1232 |
+
gap: 1.5rem;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
.section-header {
|
| 1236 |
+
margin-bottom: 1.5rem;
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
.section-title {
|
| 1240 |
+
font-size: 1.3rem;
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
.chat-container {
|
| 1244 |
+
height: 550px;
|
| 1245 |
+
border-radius: 20px;
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
.chat-messages {
|
| 1249 |
+
padding: 0.75rem;
|
| 1250 |
+
gap: 0.75rem;
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
.message-content {
|
| 1254 |
+
padding: 1rem 1.25rem;
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.message-avatar {
|
| 1258 |
+
width: 40px;
|
| 1259 |
+
height: 40px;
|
| 1260 |
+
font-size: 1rem;
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
.question-card {
|
| 1264 |
+
padding: 1rem;
|
| 1265 |
+
gap: 0.75rem;
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
.card-icon {
|
| 1269 |
+
width: 40px;
|
| 1270 |
+
height: 40px;
|
| 1271 |
+
font-size: 1rem;
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
.floating-element {
|
| 1275 |
+
display: none;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
.floating-element.el-1,
|
| 1279 |
+
.floating-element.el-2 {
|
| 1280 |
+
display: block;
|
| 1281 |
+
width: 100px;
|
| 1282 |
+
height: 100px;
|
| 1283 |
+
}
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
@media (max-width: 360px) {
|
| 1287 |
+
.container {
|
| 1288 |
+
padding: 0.5rem;
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
.chat-container {
|
| 1292 |
+
height: 500px;
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
.chat-header {
|
| 1296 |
+
padding: 0.75rem;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
.profile-avatar {
|
| 1300 |
+
width: 45px;
|
| 1301 |
+
height: 45px;
|
| 1302 |
+
font-size: 1.2rem;
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
.chat-stats {
|
| 1306 |
+
gap: 1rem;
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
.question-card {
|
| 1310 |
+
flex-direction: column;
|
| 1311 |
+
text-align: center;
|
| 1312 |
+
gap: 0.5rem;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
.card-content h3 {
|
| 1316 |
+
text-align: center;
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
.urdu-text.question-card {
|
| 1320 |
+
flex-direction: column;
|
| 1321 |
+
}
|
| 1322 |
+
|
| 1323 |
+
.urdu-text .card-content {
|
| 1324 |
+
text-align: center;
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
@media print {
|
| 1329 |
+
.floating-elements,
|
| 1330 |
+
.send-button,
|
| 1331 |
+
.typing-indicator,
|
| 1332 |
+
.chat-input-container {
|
| 1333 |
+
display: none !important;
|
| 1334 |
+
}
|
| 1335 |
+
|
| 1336 |
+
.chat-container {
|
| 1337 |
+
box-shadow: none;
|
| 1338 |
+
height: auto;
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
.chat-messages {
|
| 1342 |
+
overflow: visible;
|
| 1343 |
+
}
|
| 1344 |
+
}
|