diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,1216 +1,1960 @@
"""
-Rahbar | رہبر — Pakistan's Civic Complaint System v7.0
-HuggingFace Spaces compatible · Production Ready
+╔══════════════════════════════════════════════════════════════╗
+║ Rahbar | رہبر ║
+║ Pakistan's AI-Powered Civic Complaint System ║
+║ v3.0 — RAG Chatbot + Location Fixed + Camera Fixed ║
+╚══════════════════════════════════════════════════════════════╝
+اردو رہنمائی: یہ سسٹم کچرا، گڑھے اور پائپ لیکیج کی شکایات کے لیے ہے۔
"""
-import os, io, re, uuid, base64, datetime, urllib.parse, json
+import os, io, re, uuid, base64, datetime, urllib.parse, json, math
from PIL import Image
import gradio as gr
-# Environment variables for APIs
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
-complaint_log = []
-# ─── KNOWLEDGE BASE ───────────────────────────────────────────
-LEGAL_KNOWLEDGE = [
+complaint_log = []
+
+# ─── RAG KNOWLEDGE BASE — Dataset-inspired civic data ─────────
+# Based on: road-issues-detection-dataset, urban-issues-dataset,
+# consumer-complaints dataset — cleaned & structured for Pakistan civic context
+
+RAG_DOCUMENTS = [
+ # === GARBAGE / SANITATION ===
{
+ "id": "garbage_001",
"category": "Garbage",
- "title": "Punjab Waste Management Act 2014",
- "content": "Under Punjab Waste Management Act 2014 any citizen can file a garbage complaint. Fine: Rs.500–50,000. Local government must act within 48 hours. Authority: Solid Waste Management Board. Helpline: 1139. Citizens have the right to demand a written response and escalate to CM Portal if ignored.",
- "laws": ["Punjab Waste Management Act 2014", "EPA 1997 Section 11", "Punjab LGA 2022 Schedule II"],
+ "title": "Punjab Waste Management Act 2014 — بنیادی حقوق",
+ "content": """پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت کوئی بھی شہری کچرا پھینکنے کی شکایت کر سکتا ہے۔
+جرمانہ 500 سے 50,000 روپے ہے۔ مقامی حکومت 48 گھنٹوں میں کارروائی کرنے کی پابند ہے۔
+ذمہ دار ادارہ: Solid Waste Management Board۔ ہیلپ لائن: 1139۔
+شہری کا حق ہے کہ وہ تحریری جواب مانگے اور نہ ملنے پر CM پورٹل پر جائے۔""",
+ "laws": ["Punjab Waste Management Act 2014", "Pakistan Environmental Protection Act 1997 Section 11", "Punjab Local Government Act 2022 Schedule II"],
"hotline": "1139",
+ "authority": "Solid Waste Management Board / Local Government",
+ "response_time": "48 hours / 48 گھنٹے",
+ "fine": "Rs. 500 – 50,000"
+ },
+ {
+ "id": "garbage_002",
+ "category": "Garbage",
+ "title": "Urban Solid Waste — شہری علاقوں میں کچرے کا مسئلہ",
+ "content": """شہری علاقوں میں کچرا اٹھانا نہ کرنا ایک سنگین مسئلہ ہے۔
+Pakistan Environmental Protection Act 1997 Section 11 کے تحت کوئی بھی ادارہ ماحول کو آلودہ نہیں کر سکتا۔
+اگر ایک ہفتے سے زیادہ کچرا نہ اٹھایا جائے تو یہ Public Nuisance بنتا ہے — PPC Section 268۔
+لاہور میں LWMC (Lahore Waste Management Company) ذمہ دار ہے — 042-111-222-888۔
+کراچی میں KMC (Karachi Metropolitan Corporation) — 021-99231677۔""",
+ "laws": ["PPC Section 268 Public Nuisance", "Punjab Waste Management Act 2014", "EPA 1997 Section 11"],
+ "hotline": "1139",
+ "authority": "LWMC Lahore / KMC Karachi / Local Solid Waste Board",
"response_time": "48 hours",
- "fine": "Rs. 500 – 50,000",
- "authority": "Solid Waste Management Board / Local Government"
+ "fine": "Rs. 500 – 50,000"
+ },
+ {
+ "id": "garbage_escalation",
+ "category": "Garbage",
+ "title": "کچرے کی شکایت — ایسکیلیشن کا طریقہ",
+ "content": """اگر مقامی ادارہ 48 گھنٹوں میں کارروائی نہ کرے تو:
+1. پہلے: محلہ ناظم یا Union Council سے رابطہ کریں
+2. دوسرا: Deputy Commissioner آفس میں تحریری درخواست دیں
+3. تیسرا: وزیراعلیٰ شکایت سیل — 0800-02345 (مفت)
+4. چوتھا: Pakistan Citizen Portal — citizenportal.gov.pk
+5. پانچواں: Federal Ombudsman (وفاقی محتسب) — 051-9204551
+6. آخری: High Court میں Writ Petition دائر کریں Article 9 کے تحت
+آپ کو معاوضے کا حق ہے اگر صحت کو نقصان ہوا — EPA 1997 Section 14۔""",
+ "laws": ["Constitution Article 9 & 14", "EPA 1997 Section 14", "PPC Section 268"],
+ "hotline": "0800-02345",
+ "authority": "CM Complaints Cell / Federal Ombudsman",
+ "response_time": "3 working days",
+ "fine": "Compensation claimable"
},
+ # === POT HOLE / ROAD ===
{
+ "id": "pothole_001",
"category": "Pot Hole",
- "title": "National Highways Safety Ordinance 2000",
- "content": "Road repairs must be done within 72 hours under National Highways Safety Ordinance 2000. Vehicle damaged? Claim compensation under Motor Vehicles Ordinance 1965. NHA Helpline: 051-9032800.",
+ "title": "National Highways Safety Ordinance 2000 — سڑک کے گڑھے",
+ "content": """سڑک میں گڑھا ہونا NHA (National Highway Authority) کی ذمہ داری ہے۔
+National Highways Safety Ordinance 2000 کے تحت سڑک کی مرمت 72 گھنٹوں میں ہونی چاہیے۔
+Punjab Local Government Act 2022 Section 54 — LDA اور C&W Department بھی ذمہ دار ہیں۔
+گاڑی کو نقصان ہو تو آپ ہرجانے کا دعویٰ کر سکتے ہیں — Motor Vehicles Ordinance 1965۔
+NHA ہیلپ لائن: 051-9032800۔ LDA لاہور: 042-99230215۔""",
"laws": ["National Highways Safety Ordinance 2000", "Punjab LGA 2022 Section 54", "Motor Vehicles Ordinance 1965"],
"hotline": "051-9032800",
- "response_time": "72 hours",
- "fine": "Authority liable for vehicle damage",
- "authority": "NHA / C&W Department / LDA"
+ "authority": "NHA / C&W Department / LDA",
+ "response_time": "72 hours / 72 گھنٹے",
+ "fine": "Authority liable for vehicle damage"
},
{
+ "id": "pothole_002",
+ "category": "Pot Hole",
+ "title": "سڑک حادثہ اور گڑھے — قانونی چارہ جوئی",
+ "content": """اگر سڑک کے گڑھے کی وجہ سے حادثہ ہو تو:
+1. فوری طور پر پولیس رپورٹ درج کروائیں
+2. تصویر کھینچیں اور تاریخ محفوظ کریں
+3. NHA یا LDA کو تحریری نوٹس دیں
+4. Tort Law کے تحت Negligence کا دعویٰ عدالت میں کر سکتے ہیں
+5. Federal Ombudsman میں شکایت کریں — 051-9204551
+6. High Court Writ Petition دائر کر سکتے ہیں
+Road Quality Inspection Reports باقاعدگی سے nha.gov.pk پر شائع ہوتی ہیں۔""",
+ "laws": ["Tort Law Negligence", "NHA Safety Ordinance 2000", "Constitution Article 9"],
+ "hotline": "051-9204551",
+ "authority": "Federal Ombudsman / High Court",
+ "response_time": "Court timeline",
+ "fine": "Compensation for injury/damage"
+ },
+ # === PIPE LEAKAGE / WATER ===
+ {
+ "id": "water_001",
"category": "Pipe Leakage",
- "title": "Punjab Water Act 2019",
- "content": "Under Punjab Water Act 2019 Section 23, WASA must repair pipe leakage within 24 hours. WASA Lahore: 042-99200300. Clean water is a fundamental right as established by Supreme Court 2018.",
+ "title": "Punjab Water Act 2019 — پائپ لیکیج کے حقوق",
+ "content": """Punjab Water Act 2019 Section 23 کے تحت WASA 24 گھنٹوں میں پائپ لیکیج ٹھیک کرنے کی پابند ہے۔
+اگر 24 گھنٹوں میں مرمت نہ ہو تو آپ DC آفس میں شکایت کریں۔
+جرمانہ 10,000 سے 5,00,000 روپے ہو سکتا ہے WASA کو۔
+WASA لاہور ہیلپ لائن: 042-99200300۔
+WASA کراچی (KWSB): 021-99231677۔
+WASA راولپنڈی: 051-5594244۔
+Supreme Court 2018 فیصلے کے مطابق صاف پانی آپ کا بنیادی حق ہے — PLD 2018 SC 1۔""",
"laws": ["Punjab Water Act 2019 Section 23", "WASA Act Bylaws", "Constitution Article 9"],
"hotline": "042-99200300",
- "response_time": "24 hours",
- "fine": "Rs. 10,000 – 5,00,000",
- "authority": "WASA / Pakistan Water Authority"
+ "authority": "WASA / Pakistan Water Authority",
+ "response_time": "24 hours / 24 گھنٹے",
+ "fine": "Rs. 10,000 – 5,00,000"
+ },
+ {
+ "id": "water_002",
+ "category": "Pipe Leakage",
+ "title": "آلودہ پانی اور صحت کے مسائل — قانونی حق",
+ "content": """اگر پانی آلودہ ہو یا لیکیج کی وجہ سے گندا ہو تو:
+Pakistan Environmental Protection Act 1997 Section 13 کے تحت پانی کو آلودہ کرنا جرم ہے۔
+National Drinking Water Policy 2009 WHO معیار کے مطابق پانی فراہم کرنا لازمی ہے۔
+آلودہ پانی سے بیماری ہو تو EPA 1997 کے تحت معاوضے کا دعویٰ کر سکتے ہیں۔
+پانی کا بل غلط آئے تو WASA Ombudsman سے رابطہ کریں۔
+آپ بلنگ روک سکتے ہیں اگر پانی آلودہ ہو — WASA Bylaws کے تحت۔""",
+ "laws": ["EPA 1997 Section 13", "National Drinking Water Policy 2009", "Punjab Water Act 2019"],
+ "hotline": "042-99200300",
+ "authority": "WASA / Pakistan Water Authority / EPA",
+ "response_time": "24-48 hours",
+ "fine": "Compensation for health damage"
},
{
+ "id": "water_escalation",
+ "category": "Pipe Leakage",
+ "title": "WASA نے کارروائی نہ کی — کیا کریں؟",
+ "content": """اگر WASA نے 24 گھنٹوں میں پائپ ٹھیک نہ کیا تو یہ اقدامات کریں:
+1. WASA ہیلپ لائن پر دوبارہ کال کریں اور شکایت نمبر نوٹ کریں
+2. قریبی WASA آفس میں تحریری درخواست دیں
+3. Deputy Commissioner آفس میں شکایت درج کروائیں
+4. وزیراعلیٰ شکایت سیل: 0800-02345 (مفت کال)
+5. Pakistan Citizen Portal: citizenportal.gov.pk
+6. Pakistan Water Authority: 051-9246150
+7. Federal Ombudsman (وفاقی محتسب): 051-9204551
+8. High Court — Article 9 کے تحت Writ Petition
+یاد رکھیں: شکایت کا ثبوت محفوظ رکھیں — تصویر، تاریخ، شکایت نمبر۔""",
+ "laws": ["Punjab Water Act 2019", "Constitution Article 9", "EPA 1997"],
+ "hotline": "0800-02345",
+ "authority": "CM Complaints Cell / PWA / Federal Ombudsman",
+ "response_time": "Escalation pathway",
+ "fine": "Rs. 10,000 – 5,00,000 + compensation"
+ },
+ # === GENERAL RIGHTS ===
+ {
+ "id": "rights_001",
"category": "General",
- "title": "Fundamental Rights of Pakistani Citizens",
- "content": "Article 9: Right to Life includes clean water and clean environment. Article 14: Right to Dignity means polluted environment violates your dignity. Article 19A: Right to Information allows you to request information from any public body.",
+ "title": "پاکستانی شہری کے بنیادی حقوق — آئینی تحفظ",
+ "content": """پاکستان کے آئین کے تحت آپ کے بنیادی حقوق:
+Article 9: زندگی کا حق — اس میں صاف پانی، صاف ماحول شامل ہے (SC 2018 فیصلہ)
+Article 14: وقار کا حق — گندہ ماحول آپ کے وقار کو ٹھیس پہنچاتا ہے
+Article 19A: معلومات کا حق — آپ کسی بھی سرکاری ادارے سے معلومات مانگ سکتے ہیں
+Pakistan Citizen Portal پر شکایت کا قانونی طور پر جواب ملنا ضروری ہے۔
+FIR درج کروانے کا حق ہے اگر سرکاری ادارہ کارروائی نہ کرے۔""",
"laws": ["Constitution Article 9", "Constitution Article 14", "Constitution Article 19A"],
"hotline": "0800-02345",
- "response_time": "3 working days",
- "fine": "Authority accountable",
- "authority": "High Court / Federal Ombudsman"
- }
+ "authority": "High Court / Supreme Court / Federal Ombudsman",
+ "response_time": "3 working days (citizen portal)",
+ "fine": "Authority accountable"
+ },
+ {
+ "id": "rights_002",
+ "category": "General",
+ "title": "شکایت کیسے درج کروائیں — مکمل رہنمائی",
+ "content": """شکایت درج کروانے کا صحیح طریقہ:
+1. تصویر کھینچیں — تاریخ اور وقت کے ساتھ
+2. مقام نوٹ کریں — گلی، محلہ، شہر
+3. متعلقہ ہیلپ لائن پر کال کریں اور شکایت نمبر لیں
+4. اگر 48-72 گھنٹوں میں کارروائی نہ ہو تو CM پورٹل استعمال کریں
+5. Pakistan Citizen Portal (citizenportal.gov.pk) سب سے مؤثر طریقہ ہے
+6. WhatsApp پر شکایت شیئر کریں تاکہ لوگوں کو معلوم ہو
+اہم نمبر: کچرا 1139 | سڑک 051-9032800 | WASA 042-99200300 | CM پورٹل 0800-02345""",
+ "laws": ["Right to Information Act 2017", "Constitution Article 9", "EPA 1997"],
+ "hotline": "0800-02345",
+ "authority": "Pakistan Citizen Portal",
+ "response_time": "3-5 working days",
+ "fine": "N/A"
+ },
+ {
+ "id": "rights_003",
+ "category": "General",
+ "title": "Federal Ombudsman — وفاقی محتسب کا کردار",
+ "content": """وفاقی محتسب (Federal Ombudsman) ایک آزاد ادارہ ہے جو سرکاری اداروں کے خلاف شکایات سنتا ہے۔
+NHA، WASA، LDA، لوکل گورنمنٹ کے خلاف شکایت یہاں کر سکتے ہیں۔
+فون: 051-9204551 | ویب سائٹ: mohtasib.gov.pk
+شکایت مفت درج ہوتی ہے اور 60 دنوں میں فیصلہ آنا چاہیے۔
+اگر فیصلے سے مطمئن نہ ہوں تو صدر پاکستان کو اپیل کر سکتے ہیں۔""",
+ "laws": ["Federal Ombudsmen Institutional Reforms Act 2013"],
+ "hotline": "051-9204551",
+ "authority": "Federal Ombudsman (Mohtasib)",
+ "response_time": "60 days",
+ "fine": "Binding recommendations"
+ },
+ # === CONSUMER COMPLAINT DATA ===
+ {
+ "id": "consumer_001",
+ "category": "General",
+ "title": "صارف کی شکایات — عام مسائل اور حل",
+ "content": """پاکستان میں سب سے زیادہ شہری شکایات:
+1. پانی کی فراہمی نہ ہونا یا لیکیج (35% شکایات)
+2. سڑکوں کی خراب حالت اور گڑھے (28% شکایات)
+3. کچرا نہ اٹھانا (22% شکایات)
+4. سیوریج کا مسئلہ (15% شکایات)
+Urban Issues Dataset کے مطابق لاہور، کراچی اور راولپنڈی میں یہ مسائل سب سے زیادہ ہیں۔
+زیادہ تر مسائل رپورٹ کرنے کے بعد 72 گھنٹوں میں حل ہو جاتے ہیں اگر صحیح طریقے سے شکایت کی جائے۔""",
+ "laws": ["Punjab LGA 2022", "Punjab Water Act 2019", "Waste Management Act 2014"],
+ "hotline": "0800-02345",
+ "authority": "Local Government / WASA / SWMB",
+ "response_time": "48-72 hours",
+ "fine": "Varies by issue"
+ },
+ {
+ "id": "road_dataset_001",
+ "category": "Pot Hole",
+ "title": "Road Issues Detection — سڑک کے مسائل کی اقسام",
+ "content": """Road Issues Detection Dataset کے مطابق پاکستانی سڑکوں پر عام مسائل:
+- Pothole (گڑھا): سب سے عام، 45% road complaints
+- Cracking (دراڑیں): 25% road complaints — بارش میں خطرناک
+- Rutting (پہیوں کے نشان): 15% complaints — بھاری گاڑیوں سے
+- Raveling (سطح اکھڑنا): 10% complaints
+- Depression (دھنسنا): 5% complaints
+اگر گڑھا 10 سینٹی میٹر سے گہرا ہو تو فوری خطرہ ہے — NHA کو اطلاع دیں۔
+شکایت کے ساتھ GPS کوآرڈینیٹس اور تصویر لازمی بھیجیں۔""",
+ "laws": ["NHA Safety Ordinance 2000", "Punjab LGA 2022 Section 54"],
+ "hotline": "051-9032800",
+ "authority": "NHA / C&W / LDA",
+ "response_time": "72 hours",
+ "fine": "Authority liable"
+ },
]
-# ─── STATIC DATA ──────────────────────────────────────────────
+# ─── RAG ENGINE ───────────────────────────────────────────────
+class RAGEngine:
+ def __init__(self):
+ self.documents = RAG_DOCUMENTS
+ self.embeddings = None
+ self.index = None
+ self.model = None
+ self._initialized = False
+
+ def initialize(self):
+ """Lazy initialize — only when first query comes in"""
+ if self._initialized:
+ return True
+ try:
+ from sentence_transformers import SentenceTransformer
+ import faiss
+ import numpy as np
+
+ self.model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
+ texts = [f"{d['title']} {d['content']}" for d in self.documents]
+ self.embeddings = self.model.encode(texts, convert_to_numpy=True)
+ dimension = self.embeddings.shape[1]
+ self.index = faiss.IndexFlatIP(dimension)
+ # Normalize for cosine similarity
+ norms = np.linalg.norm(self.embeddings, axis=1, keepdims=True)
+ normalized = self.embeddings / (norms + 1e-9)
+ self.index.add(normalized.astype('float32'))
+ self._initialized = True
+ return True
+ except Exception as e:
+ print(f"RAG init error: {e}")
+ return False
+
+ def retrieve(self, query, top_k=3):
+ """Retrieve top-k relevant documents for a query"""
+ if not self._initialized:
+ if not self.initialize():
+ return self._keyword_fallback(query)
+ try:
+ import numpy as np
+ q_emb = self.model.encode([query], convert_to_numpy=True)
+ norm = np.linalg.norm(q_emb, axis=1, keepdims=True)
+ q_norm = (q_emb / (norm + 1e-9)).astype('float32')
+ scores, indices = self.index.search(q_norm, top_k)
+ results = []
+ for idx, score in zip(indices[0], scores[0]):
+ if 0 <= idx < len(self.documents) and score > 0.2:
+ doc = self.documents[idx].copy()
+ doc['relevance_score'] = float(score)
+ results.append(doc)
+ return results if results else self._keyword_fallback(query)
+ except Exception as e:
+ return self._keyword_fallback(query)
+
+ def _keyword_fallback(self, query):
+ """Simple keyword matching when vector search unavailable"""
+ q = query.lower()
+ matched = []
+ keywords = {
+ "garbage": ["garbage", "kachra", "کچرا", "waste", "sanitation", "trash"],
+ "pothole": ["pothole", "pot hole", "sadak", "سڑک", "road", "gara", "گڑھا"],
+ "water": ["water", "pani", "پانی", "wasa", "pipe", "leakage", "لیکیج"],
+ }
+ category_map = {"garbage": "Garbage", "pothole": "Pot Hole", "water": "Pipe Leakage"}
+ found_cat = None
+ for cat, kws in keywords.items():
+ if any(kw in q for kw in kws):
+ found_cat = category_map[cat]
+ break
+ for doc in self.documents:
+ if found_cat and doc['category'] == found_cat:
+ matched.append(doc)
+ elif doc['category'] == 'General':
+ matched.append(doc)
+ return matched[:3] if matched else self.documents[:3]
+
+ def format_context(self, docs):
+ """Format retrieved docs into context string for LLM"""
+ if not docs:
+ return ""
+ context = "متعلقہ قانونی معلومات (RAG Knowledge Base سے):\n\n"
+ for i, doc in enumerate(docs, 1):
+ context += f"[{i}] {doc['title']}\n"
+ context += f"مواد: {doc['content'][:400]}\n"
+ context += f"قوانین: {', '.join(doc['laws'][:2])}\n"
+ context += f"ہیلپ لائن: {doc['hotline']} | جواب: {doc['response_time']}\n\n"
+ return context
+
+
+# Global RAG instance
+rag_engine = RAGEngine()
+
CITIES_AREAS = {
- "Lahore": ["Model Town", "DHA", "Gulberg", "Johar Town", "Bahria Town", "Township", "Cantonment"],
- "Karachi": ["Clifton", "DHA", "Gulshan-e-Iqbal", "PECHS", "Korangi", "Saddar", "North Nazimabad"],
- "Islamabad": ["F-7", "F-8", "F-10", "G-9", "G-10", "G-11", "Blue Area"],
+ "Lahore": ["Model Town", "DHA", "Gulberg", "Johar Town", "Bahria Town", "Township", "Cantonment"],
+ "Karachi": ["Clifton", "DHA", "Gulshan-e-Iqbal", "PECHS", "Korangi", "Saddar", "North Nazimabad"],
+ "Islamabad": ["F-7", "F-8", "F-10", "G-9", "G-10", "G-11", "Blue Area"],
"Rawalpindi": ["Saddar", "Bahria Town", "Chaklala", "Satellite Town", "Murree Road"],
"Faisalabad": ["Jinnah Colony", "Madina Town", "Peoples Colony", "Ghulam Muhammad Abad", "Susan Road"],
- "Multan": ["Shah Rukn-e-Alam", "Cantt", "Gulgasht Colony", "New Multan", "Bosan Road"],
- "Peshawar": ["Hayatabad", "University Town", "Cantt", "Saddar", "Gulbahar"],
- "Quetta": ["Satellite Town", "Jinnah Town", "Cantt", "Sariab Road", "Brewery Road"],
+ "Multan": ["Shah Rukn-e-Alam", "Cantt", "Gulgasht Colony", "New Multan", "Bosan Road"],
+ "Peshawar": ["Hayatabad", "University Town", "Cantt", "Saddar", "Gulbahar"],
+ "Quetta": ["Satellite Town", "Jinnah Town", "Cantt", "Sariab Road", "Brewery Road"],
}
+
ISSUE_TYPES = ["🗑️ Garbage", "🕳️ Pot Hole", "💧 Pipe Leakage"]
-LANGUAGES = ["English", "اردو (Urdu)", "پنجابی (Punjabi)", "سندھی (Sindhi)"]
-LANG_CODES = {"English": "en", "اردو (Urdu)": "ur", "پنجابی (Punjabi)": "ur", "سندھی (Sindhi)": "ur"}
+LANGUAGES = ["اردو (Urdu)", "English", "پنجابی (Punjabi)", "سندھی (Sindhi)"]
-LEGAL_INFO = {
+LEGAL_KB = {
"Garbage": {
- "laws": ["Punjab Waste Management Act 2014", "EPA 1997 Section 11", "Punjab LGA 2022 Schedule II", "PPC Section 268"],
- "fine": "Rs. 500–50,000",
+ "laws": [
+ "Punjab Waste Management Act 2014",
+ "Pakistan Environmental Protection Act 1997 (Section 11)",
+ "Punjab Local Government Act 2022 (Schedule II – Sanitation Duties)",
+ "Pakistan Penal Code Section 268 – Public Nuisance",
+ ],
+ "fine": "Rs. 500 – 50,000 (per offence)",
"authority": "Local Government / Solid Waste Management Board",
- "hotline": "1139",
- "response": "48 hours",
+ "hotline": "1139",
+ "response": "48 hours / 48 گھنٹے",
"citizen_rights": [
- "Right to clean environment (Constitution Article 9 & 14)",
- "Right to file FIR under PPC Section 268 if authority fails",
- "Right to compensation for health damage under EPA 1997",
- "Right to written response within 3 working days",
+ "صاف ماحول کا حق — آئین Article 9 & 14",
+ "اگر ادارہ کارروائی نہ کرے تو FIR کا حق — PPC Section 268",
+ "صحت کو نقصان پر معاوضے کا حق — EPA 1997",
+ "تحریری جواب مانگنے کا حق — 3 کاری دن میں",
],
- "escalation": "CM Complaints Cell: 0800-02345 | citizenportal.gov.pk"
+ "escalation": "وزیراعلیٰ شکایت: 0800-02345 | citizenportal.gov.pk",
+ "dataset_ref": "Punjab Solid Waste Management Board | Urban Issues Dataset",
},
"Pot Hole": {
- "laws": ["National Highways Safety Ordinance 2000", "Punjab LGA 2022 Section 54", "Motor Vehicles Ordinance 1965"],
- "fine": "Authority liable for vehicle damage & injury",
- "authority": "NHA / C&W Department / LDA",
- "hotline": "051-9032800",
- "response": "72 hours",
+ "laws": [
+ "National Highways Safety Ordinance 2000",
+ "Punjab Local Government Act 2022 (Section 54 – Road Maintenance)",
+ "Motor Vehicles Ordinance 1965 (Road Authority Liability)",
+ "Tort Law – Negligence",
+ ],
+ "fine": "گاڑی کو نقصان اور ذاتی چوٹ پر ہرجانہ",
+ "authority": "National Highway Authority (NHA) / C&W Department / LDA",
+ "hotline": "051-9032800",
+ "response": "72 hours / 72 گھنٹے",
"citizen_rights": [
- "Right to compensation for vehicle damage or personal injury",
- "Right to lodge complaint with Federal Ombudsman: 051-9204551",
- "Right to file writ petition in High Court",
- "Right to written notice to NHA/LDA",
+ "گاڑی یا ذاتی نقصان پر معاوضے کا حق",
+ "Federal Ombudsman میں شکایت کا حق — 051-9204551",
+ "High Court میں Writ Petition دائر کرنے کا حق",
+ "NHA کو تحریری نوٹس دینے کا حق",
],
- "escalation": "Federal Ombudsman: 051-9204551 | nha.gov.pk"
+ "escalation": "Federal Ombudsman: 051-9204551 | nha.gov.pk",
+ "dataset_ref": "NHA Road Quality Reports | Road Issues Detection Dataset",
},
"Pipe Leakage": {
- "laws": ["Punjab Water Act 2019 Section 23", "WASA Act Bylaws", "EPA 1997 Section 13", "Constitution Article 9"],
- "fine": "Rs. 10,000–5,00,000 under PWA 2019",
+ "laws": [
+ "Punjab Water Act 2019 (Section 23 – Supply Obligation)",
+ "WASA Act – Water & Sanitation Agency Bylaws",
+ "Pakistan Environmental Protection Act 1997 (Section 13)",
+ "Punjab Local Government Act 2022",
+ "Constitution of Pakistan Article 9",
+ ],
+ "fine": "Rs. 10,000 – 5,00,000 under PWA 2019",
"authority": "WASA / Pakistan Water Authority",
- "hotline": "042-99200300",
- "response": "24 hours",
+ "hotline": "042-99200300",
+ "response": "24 hours / 24 گھنٹے",
"citizen_rights": [
- "Right to safe drinking water (Supreme Court 2018)",
- "Right to compensation for property damage",
- "Right to stop billing if water is contaminated",
- "Right to file complaint with Pakistan Water Authority",
+ "صاف پانی کا حق — SC 2018 فیصلہ PLD 2018 SC 1",
+ "جائیداد کے نقصان پر معاوضے کا حق",
+ "آلودہ پانی کی صورت میں بل روکنے کا حق",
+ "Pakistan Water Authority میں شکایت کا حق",
],
- "escalation": "Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345"
+ "escalation": "Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345",
+ "dataset_ref": "WASA Annual Service Quality Reports | Consumer Complaints Dataset",
},
}
LOCALIZED = {
"Garbage": {
- "English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs.500–50,000. Helpline: 1139",
- "اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500–50,000 روپے۔ ہیلپ لائن: 1139",
- "پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ 500 توں 50,000 روپے۔",
- "سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. جرمانو 500 کان 50,000 رپيا.",
+ "اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500 سے 50,000 روپے۔ ہیلپ لائن: 1139",
+ "English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs. 500-50,000. Helpline: 1139",
+ "پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ: 500 توں 50,000 روپے۔",
+ "سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. ڏنڊ: 500 کان 50,000 رپيا.",
},
"Pot Hole": {
- "English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800",
- "اردو (Urdu)": "سڑک کی مرمت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔ NHA: 051-9032800",
- "پنجابی (Punjabi)": "سڑک دی مرمت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔",
- "سندھی (Sindhi)": "سڙڪ جي مرمت 72 ڪلاڪن �� حڪومت جي ذميواري آهي.",
+ "اردو (Urdu)": "سڑک کی مرمت پنجاب مقامی حکومت ایکٹ 2022 کے تحت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔ NHA: 051-9032800",
+ "English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800",
+ "پنجابی (Punjabi)": "سڑک دی مرمت پنجاب LGA 2022 دے تحت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔",
+ "سندھی (Sindhi)": "سڙڪ جي مرمت پنجاب LGA 2022 تحت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي.",
},
"Pipe Leakage": {
- "English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300",
- "اردو (Urdu)": "پائپ لیکیج WASA کی 24 گھنٹوں میں ذمہ داری ہے۔ WASA: 042-99200300",
- "پنجابی (Punjabi)": "پائپ لیکیج دی مرمت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔",
- "سندھی (Sindhi)": "پائپ ليڪيج جي مرمت WASA جي 24 ڪلاڪن ۾ ذميواري آهي.",
+ "اردو (Urdu)": "پائپ لیکیج کی مرمت پنجاب واٹر ایکٹ 2019 کے تحت WASA کی 24 گھنٹوں میں ذمہ داری ہے۔ WASA: 042-99200300",
+ "English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300",
+ "پنجابی (Punjabi)": "پائپ لیکیج دی مرمت پنجاب واٹر ایکٹ 2019 دے تحت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔",
+ "سندھی (Sindhi)": "پائپ ليڪيج جي مرمت پنجاب واٽر ايڪٽ 2019 تحت WASA جي 24 ڪلاڪن ۾ ذميواري آهي.",
},
}
+WASTE_CLASS_IDS = {24,25,26,27,28,32,33,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54}
-# ─── IMAGE ANALYSIS ───────────────────────────────────────────
-def analyze_image(image_pil, issue_type):
- """Basic image analysis without external APIs"""
+LANG_CODES = {
+ "اردو (Urdu)": "ur",
+ "English": "en",
+ "پنجابی (Punjabi)": "pa",
+ "سندھی (Sindhi)": "sd",
+}
+
+# ─── YOLO ─────────────────────────────────────────────────────
+def detect_with_yolo(image_pil, issue_type):
try:
- if image_pil is None:
- return None, "No image provided", 5, "REJECTED", "Please upload an image", "", "0%", ""
-
- width, height = image_pil.size
- issue_clean = issue_type.split(" ", 1)[-1]
-
- if issue_clean == "Garbage":
- status = "APPROVED"
- reason = "Image shows ground-level content consistent with waste disposal areas."
- reason_urdu = "تصویر میں زمینی سطح کا مواد نظر آ رہا ہے جو فضلہ پھینکنے کے علاقوں سے مطابقت رکھتا ہے۔"
- severity = 7
- confidence = "85%"
- action = "Inspect the area and arrange immediate waste removal."
- elif issue_clean == "Pot Hole":
- status = "APPROVED"
- reason = "Image shows road surface with possible damage patterns."
- reason_urdu = "تصویر میں سڑک کی سطح کو نقصان کے ممکنہ نمونوں کے ساتھ دکھایا گیا ہے۔"
- severity = 6
- confidence = "80%"
- action = "Conduct road inspection and schedule repair work within 72 hours."
- elif issue_clean == "Pipe Leakage":
- status = "APPROVED"
- reason = "Image shows water-related surface conditions."
- reason_urdu = "تصویر میں پانی سے متعلق سطح کے حالات دکھائے گئے ہیں۔"
- severity = 7
- confidence = "82%"
- action = "Deploy WASA team for immediate inspection and repair."
- else:
- status = "APPROVED"
- reason = "Image verification completed."
- reason_urdu = "تصویر کی تصدیق مکمل ہوگئی۔"
- severity = 5
- confidence = "75%"
- action = "Forward to relevant department for action."
-
- return image_pil, f"{issue_clean} area identified.", severity, status, reason, reason_urdu, confidence, action
+ from ultralytics import YOLO
+ import numpy as np
+ model = YOLO("yolo26n.pt")
+ img_np = np.array(image_pil)
+ results = model(img_np, verbose=False)
+ result = results[0]
+ names = model.names
+ detected = []
+ severity = 1
+ clean_issue = issue_type.split(" ", 1)[-1]
+ for box in result.boxes:
+ cls_id = int(box.cls[0])
+ conf = float(box.conf[0])
+ label = names.get(cls_id, f"class_{cls_id}")
+ detected.append(f"{label} ({conf:.0%})")
+ if clean_issue == "Garbage" and cls_id in WASTE_CLASS_IDS:
+ severity = min(10, severity + 2)
+ elif clean_issue in ("Pot Hole", "Pipe Leakage"):
+ severity = min(10, severity + 1)
+ annotated_np = result.plot()
+ annotated = Image.fromarray(annotated_np)
+ summary = (f"Detected {len(detected)} object(s): {', '.join(detected[:5])}"
+ if detected else "No specific objects detected by YOLO.")
+ return annotated, summary, max(severity, 3)
+ except ImportError:
+ return image_pil, "YOLO not installed – skipping object detection.", 5
except Exception as e:
- return image_pil, f"Analysis completed", 5, "APPROVED", "Image processed successfully", "", "90%", "Proceed with complaint registration."
+ return image_pil, f"YOLO error: {e}", 5
+# ─── GEMINI ───────────────────────────────────────────────────
+def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
+ if not GOOGLE_API_KEY:
+ return "WARNING GOOGLE_API_KEY not set. Gemini analysis skipped."
+ try:
+ import google.generativeai as genai
+ genai.configure(api_key=GOOGLE_API_KEY)
+ model = genai.GenerativeModel("gemini-3-flash-preview ")
+ buf = io.BytesIO()
+ image_pil.save(buf, format="JPEG")
+ image_bytes = buf.getvalue()
+ prompt = f"""You are a STRICT Pakistani Civic Issue Inspector AI.
+REPORTED ISSUE: '{issue}' | CITY: {city} | LOCATION: {location}
+YOLO: {yolo_summary}
+
+Check if image shows: Garbage=actual waste/litter, Pot Hole=visible road hole, Pipe Leakage=water from pipe.
+Clean/indoor/person-only/vegetation REJECT. Be strict.
+
+Respond ONLY in this format:
+STATUS: [APPROVED or REJECTED]
+REASON: [2-3 sentences English]
+REASON_URDU: [2-3 sentences Urdu]
+SEVERITY: [1-10]
+CONFIDENCE: [XX%]
+RECOMMENDED_ACTION: [one sentence]"""
+ image_part = {"mime_type": "image/jpeg", "data": base64.b64encode(image_bytes).decode()}
+ response = model.generate_content([prompt, image_part])
+ return response.text.strip()
+ except Exception as e:
+ return f"WARNING Gemini error: {e}"
+
+def parse_gemini_response(gemini_text):
+ r = {"status":"UNKNOWN","reason":"Could not parse.","reason_urdu":"","severity":5,"confidence":"0%","action":""}
+ if not gemini_text: return r
+ m = re.search(r"STATUS:\s*(APPROVED|REJECTED)", gemini_text, re.IGNORECASE)
+ if m: r["status"] = m.group(1).upper()
+ m = re.search(r"SEVERITY:\s*(\d+)", gemini_text)
+ if m: r["severity"] = int(m.group(1))
+ m = re.search(r"CONFIDENCE:\s*(\d+%)", gemini_text, re.IGNORECASE)
+ if m: r["confidence"] = m.group(1)
+ m = re.search(r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)", gemini_text, re.DOTALL|re.IGNORECASE)
+ if m: r["reason"] = m.group(1).strip()
+ m = re.search(r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", gemini_text, re.DOTALL|re.IGNORECASE)
+ if m: r["reason_urdu"] = m.group(1).strip()
+ m = re.search(r"RECOMMENDED_ACTION:\s*(.+?)(?=$)", gemini_text, re.DOTALL)
+ if m: r["action"] = m.group(1).strip()
+ return r
+
+# ─── LLAMA 3 LEGAL ADVICE ─────────────────────────────────────
+def analyze_with_llama(issue, location, city, yolo_summary, severity, language="اردو (Urdu)"):
+ clean = issue.split(" ", 1)[-1]
+ kb = LEGAL_KB.get(clean, {})
+
+ lang_map = {
+ "اردو (Urdu)": "مکمل جواب اردو میں دیں۔",
+ "English": "Respond in clear professional English.",
+ "پنجابی (Punjabi)": "پنجابی (شاہ مکھی) وچ جواب دیو۔",
+ "سندھی (Sindhi)": "سنڌي ۾ جواب ڏيو.",
+ }
+ lang_instruction = lang_map.get(language, "مکمل جواب اردو میں دیں۔")
-# ─── LEGAL ADVICE ─────────────────────────────────────────────
-def get_legal_advice(issue, location, city, severity, language="English"):
- issue_clean = issue.split(" ", 1)[-1]
- info = LEGAL_INFO.get(issue_clean, {})
-
if not GROQ_API_KEY:
- # Provide detailed legal information without AI
- rights_text = "\n".join(f"• {r}" for r in info.get("citizen_rights", []))
- laws_text = "\n".join(f"• {l}" for l in info.get("laws", []))
-
- if language == "اردو (Urdu)":
- return f"""## قانونی معلومات - {issue}
-
-**قابل اطلاق قوانین:**
-{laws_text}
-
-**آپ کے حقوق:**
-{rights_text}
-
-**ذمہ دار اتھارٹی:** {info.get('authority', 'N/A')}
-**ہیلپ لائن:** {info.get('hotline', 'N/A')}
-**جوابی وقت:** {info.get('response', 'N/A')}
-**جرمانہ:** {info.get('fine', 'N/A')}
-
-**اگر نظر انداز کیا جائے تو:** {info.get('escalation', 'N/A')}"""
-
- return f"""## Legal Information - {issue}
-
-**Applicable Laws:**
-{laws_text}
-
-**Your Rights:**
-{rights_text}
-
-**Responsible Authority:** {info.get('authority', 'N/A')}
-**Helpline:** {info.get('hotline', 'N/A')}
-**Response Time:** {info.get('response', 'N/A')}
-**Fine/Penalty:** {info.get('fine', 'N/A')}
-
-**If Ignored - Escalate To:** {info.get('escalation', 'N/A')}"""
-
+ rights = "\n".join(f" • {r}" for r in kb.get("citizen_rights", []))
+ return (
+ "متعلقہ قوانین:\n" + "\n".join(f" • {l}" for l in kb.get("laws",[])) +
+ f"\n\nشہری کے حقوق:\n{rights}" +
+ f"\n\nجرمانہ/سزا: {kb.get('fine','N/A')}" +
+ f"\nہیلپ لائن: {kb.get('hotline','N/A')}" +
+ f"\nمتوقع جواب: {kb.get('response','N/A')}" +
+ f"\n\nایسکیلیشن: {kb.get('escalation','N/A')}" +
+ "\n\n(GROQ_API_KEY سیٹ کریں مکمل قانونی رہنمائی کے لیے)"
+ )
try:
from groq import Groq
-
- lang_instruction = {
- "English": "Respond in clear professional English.",
- "اردو (Urdu)": "Respond entirely in Urdu language.",
- "پنجابی (Punjabi)": "Respond in Punjabi Shahmukhi script.",
- "سندھی (Sindhi)": "Respond in Sindhi language.",
- }.get(language, "Respond in English.")
-
- prompt = f"""You are a Pakistani civic law expert providing assistance to citizens.
+ client = Groq(api_key=GROQ_API_KEY)
+ prompt = f"""آپ رہبر پلیٹ فارم کے پاکستانی شہری قانون کے ماہر ہیں۔
{lang_instruction}
-Complaint: {issue} at {location}, {city} | Severity: {severity}/10
-
-Laws applicable: {', '.join(info.get('laws', []))}
-Response time required: {info.get('response', '72 hours')}
-
-Please provide:
-1. Specific legal rights (cite law and section)
-2. Exact numbered steps to file complaint
-3. What to do if authority doesn't respond in time
-4. Possible compensation available
-5. Helplines and escalation path
+شکایت: {issue} — مقام: {location}, {city} | شدت: {severity}/10
+متعلقہ قوانین: {', '.join(kb.get('laws',[]))}
+جواب کا وقت: {kb.get('response','72 hours')}
-Be concise, practical, and helpful."""
+درج ذیل ساختی قانونی رہنمائی دیں:
+1. شہری کے مخصوص قانونی حقوق (قانون/دفعہ کا حوالہ دیں)
+2. باقاعدہ شکایت درج کروانے کے اقدامات (نمبر وار)
+3. اگر ادارہ وقت پر جواب نہ دے تو کیا کریں
+4. ممکنہ معاوضہ یا کارروائی
+5. متعلقہ ہیلپ لائنز اور ایسکیلیشن رابطے
- client = Groq(api_key=GROQ_API_KEY)
- response = client.chat.completions.create(
+عام پاکستانی شہری کے لیے مختصر اور عملی جواب دیں۔"""
+ chat = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": prompt}],
- max_tokens=700,
- temperature=0.7
+ max_tokens=700
)
- return response.choices[0].message.content.strip()
+ return chat.choices[0].message.content.strip()
except Exception as e:
- # Fallback to static information
- rights_text = "\n".join(f"• {r}" for r in info.get("citizen_rights", []))
- return f"""## Legal Information - {issue}
-
-**Your Rights:**
-{rights_text}
-
-**Helpline:** {info.get('hotline', 'N/A')}
-**Response Time:** {info.get('response', 'N/A')}
-
-**Escalation Path:** {info.get('escalation', 'CM Portal: 0800-02345')}"""
-
-
-# ─── LEGAL ASSISTANT (CHATBOT) ────────────────────────────────
-def find_relevant_info(query):
- """Find relevant legal information based on query keywords"""
- if not query:
- return LEGAL_KNOWLEDGE[:2]
-
- query_lower = query.lower()
- categories = {
- "garbage": ["garbage", "waste", "kachra", "1139", "sanitation", "dump", "trash"],
- "pothole": ["pothole", "road", "nha", "sadak", "gara", "repair", "damage"],
- "water": ["water", "wasa", "pipe", "leakage", "pani", "contaminated", "supply"],
- "rights": ["right", "fundamental", "article", "constitution", "law", "legal"],
- "ombudsman": ["ombudsman", "mohtasib", "federal ombudsman", "complaint"],
- }
-
- matched = []
- for cat, keywords in categories.items():
- if any(k in query_lower for k in keywords):
- matched.append(cat)
-
- relevant = []
- for info in LEGAL_KNOWLEDGE:
- if any(m in info['category'].lower() for m in matched) or not matched:
- relevant.append(info)
-
- return relevant[:2] if relevant else LEGAL_KNOWLEDGE[:2]
-
-def legal_chatbot(user_message, history, language):
+ return f"Llama 3 error: {e}"
+
+# ─── RAG CHATBOT — Gradio 6+ dict format ─────────────────────
+def legal_chatbot_rag(user_message, history, language):
if history is None:
history = []
- if not user_message or not user_message.strip():
- return history, ""
-
- relevant = find_relevant_info(user_message)
-
- if not GROQ_API_KEY:
- # Provide response from knowledge base
- response = ""
- for item in relevant:
- response += f"**{item['title']}**\n\n{item['content']}\n\n"
- response += f"**Helpline:** {item['hotline']}\n"
- response += f"**Response Time:** {item['response_time']}\n\n"
-
- if not response:
- response = "I can help you with issues related to garbage collection, road repairs, water supply, pipe leakages, and your legal rights as a citizen of Pakistan. Please describe your specific issue."
-
- history.append({"role": "user", "content": user_message})
- history.append({"role": "assistant", "content": response})
+ if not user_message.strip():
return history, ""
-
- try:
- from groq import Groq
-
- lang_instruction = {
- "English": "Respond in professional English. Be helpful and concise.",
- "اردو (Urdu)": "Respond entirely in Urdu language. Use proper Urdu script.",
- "پنجابی (Punjabi)": "Respond in Punjabi Shahmukhi script.",
- "سندھی (Sindhi)": "Respond in Sindhi language.",
- }.get(language, "Respond in English.")
-
- context = ""
- for item in relevant:
- context += f"- {item['title']}: {item['content'][:200]}...\n"
-
- system_prompt = f"""You are a civic rights advisor for Pakistani citizens.
+
+ # Retrieve relevant documents via RAG
+ retrieved_docs = rag_engine.retrieve(user_message, top_k=3)
+ rag_context = rag_engine.format_context(retrieved_docs)
+
+ lang_map = {
+ "اردو (Urdu)": "مکمل جواب صرف اردو میں دیں۔",
+ "English": "Respond in clear professional English.",
+ "پنجابی (Punjabi)": "پنجابی (شاہ مکھی) وچ جواب دیو۔",
+ "سندھی (Sindhi)": "سنڌي ۾ جواب ڏيو.",
+ }
+ lang_instruction = lang_map.get(language, "مکمل جواب اردو میں دیں۔")
+
+ system_content = f"""آپ رہبر کے قانونی معاون ہیں — پاکستانی شہریوں کے حقوق کے مشیر۔
{lang_instruction}
-You help with: water supply, WASA, pipe leakage, garbage collection, solid waste, roads, potholes, NHA, and Pakistani civic law.
+آپ کا علم RAG نالج بیس سے آتا ہے جس میں یہ datasets شامل ہیں:
+- Road Issues Detection Dataset (سڑک کے مسائل)
+- Urban Issues Dataset (شہری مسائل)
+- Consumer Complaints Dataset (صارف شکایات)
+- پاکستانی قوانین کا مجموعہ
+
+صرف ان موضوعات پر بات کریں: پانی، پائپ لیکیج، WASA، کچرا، سڑک، گڑھے، پاکستانی شہری قانون۔
+ہمیشہ مخصوص قانون اور ہیلپ لائن نمبر دیں۔
+زیادہ سے زیادہ 250 الفاظ میں جواب دیں۔
+
+RAG Context (Knowledge Base سے):
+{rag_context}"""
-Always cite specific laws and provide helpline numbers when relevant.
-Keep responses under 250 words and be practical.
+ if not GROQ_API_KEY:
+ # Use RAG context directly without LLM
+ if retrieved_docs:
+ doc = retrieved_docs[0]
+ answer = f"📚 نالج بیس سے جواب:\n\n"
+ answer += f"**{doc['title']}**\n\n"
+ answer += doc['content'][:500] + "\n\n"
+ answer += f"🔴 ہیلپ لائن: {doc['hotline']}\n"
+ answer += f"⏱️ جواب کا وقت: {doc['response_time']}\n"
+ answer += f"⚖️ قوانین: {', '.join(doc['laws'][:2])}\n"
+ answer += "\n_(مزید تفصیلی جواب کے لیے GROQ_API_KEY سیٹ کریں)_"
+ else:
+ answer = "میں پانی، کچرا اور سڑک کے مسائل میں مدد کر سکتا ہوں۔ براہ کرم مزید وضاحت کریں۔"
-Reference information:
-{context}"""
+ return history + [
+ {"role": "user", "content": user_message},
+ {"role": "assistant", "content": answer},
+ ], ""
- messages = [{"role": "system", "content": system_prompt}]
- for msg in history[-10:]:
- messages.append({"role": msg["role"], "content": msg["content"]})
- messages.append({"role": "user", "content": user_message})
-
+ try:
+ from groq import Groq
client = Groq(api_key=GROQ_API_KEY)
- response = client.chat.completions.create(
+ api_messages = [{"role": "system", "content": system_content}]
+ for msg in history[-16:]:
+ api_messages.append({"role": msg["role"], "content": msg["content"]})
+ api_messages.append({"role": "user", "content": user_message})
+
+ resp = client.chat.completions.create(
model="llama-3.3-70b-versatile",
- messages=messages,
- max_tokens=500,
- temperature=0.7
+ messages=api_messages,
+ max_tokens=500
)
- answer = response.choices[0].message.content.strip()
-
+ answer = resp.choices[0].message.content.strip()
+
+ # Append relevant doc references
+ if retrieved_docs:
+ refs = [f"[{d['title'][:30]}...]" for d in retrieved_docs[:2]]
+ answer += f"\n\n_📚 ماخذ: {' | '.join(refs)}_"
+
except Exception as e:
- answer = f"I apologize, but I'm having trouble connecting. Here's what I know:\n\n"
- for item in relevant:
- answer += f"• {item['title']}: {item['hotline']} (Response: {item['response_time']})\n"
-
- history.append({"role": "user", "content": user_message})
- history.append({"role": "assistant", "content": answer})
- return history, ""
-
-
-# ─── VOICE FUNCTIONS ──────────────────────────────────────────
-def text_to_speech(text, language):
- """Convert text to speech using gTTS"""
- if not text:
- return None
-
+ answer = f"Error: {e}"
+
+ return history + [
+ {"role": "user", "content": user_message},
+ {"role": "assistant", "content": answer},
+ ], ""
+
+# ─── TTS ──────────────────────────────────────────────────────
+def make_tts(text, language):
try:
from gtts import gTTS
- # Clean text (remove markdown and special characters)
- clean_text = re.sub(r'[*_#`]', '', str(text))
- clean_text = re.sub(r'\[.*?\]', '', clean_text)
- clean_text = re.sub(r'\*+', '', clean_text)
- clean_text = clean_text.strip()
-
- if not clean_text:
- return None
-
- lang_code = LANG_CODES.get(language, "en")
- tts = gTTS(text=clean_text[:500], lang=lang_code, slow=False)
- audio_path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
- tts.save(audio_path)
- return audio_path
+ lang_code = LANG_CODES.get(language, "ur")
+ tts = gTTS(text=str(text)[:600], lang=lang_code, slow=False)
+ path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
+ tts.save(path)
+ return path
except Exception as e:
print(f"TTS error: {e}")
return None
-def speech_to_text(audio_file):
- """Convert speech to text using whisper or fallback"""
+# ─── STT ──────────────────────────────────────────────────────
+def stt(audio_file):
if audio_file is None:
- return "No audio recorded. Please record or upload audio first."
-
- def convert_to_wav(input_path):
- if input_path.lower().endswith(".wav"):
- return input_path
- try:
- from pydub import AudioSegment
- output_path = input_path + "_converted.wav"
- AudioSegment.from_file(input_path).export(output_path, format="wav")
- return output_path
- except:
- return input_path
-
- if GROQ_API_KEY:
- try:
- from groq import Groq
- wav_path = convert_to_wav(audio_file)
- with open(wav_path, "rb") as file:
- client = Groq(api_key=GROQ_API_KEY)
- transcription = client.audio.transcriptions.create(
- model="whisper-large-v3",
- file=file,
- response_format="text"
- )
- text = transcription if isinstance(transcription, str) else transcription.text
- if text and text.strip():
- return text.strip()
- except Exception as e:
- print(f"Groq STT error: {e}")
-
- # Fallback
+ return "آڈیو فراہم نہیں کیا گیا۔ پہلے ریکارڈ یا اپلوڈ کریں۔"
try:
import speech_recognition as sr
- wav_path = convert_to_wav(audio_file)
recognizer = sr.Recognizer()
- with sr.AudioFile(wav_path) as source:
- recognizer.adjust_for_ambient_noise(source, duration=0.3)
- audio_data = recognizer.record(source)
+ with sr.AudioFile(audio_file) as src:
+ recognizer.adjust_for_ambient_noise(src, duration=0.5)
+ audio_data = recognizer.record(src)
try:
- return recognizer.recognize_google(audio_data, language="ur-PK")
+ text = recognizer.recognize_google(audio_data, language="ur-PK")
except:
- return recognizer.recognize_google(audio_data)
+ text = recognizer.recognize_google(audio_data)
+ return text
except Exception as e:
- return f"Could not transcribe audio. Please try typing your question."
+ return f"آواز نہیں سمجھ آئی۔ واضح آواز میں دوبارہ کوشش کریں۔ خرابی: {e}"
-def voice_to_chatbot(audio_file, history, language):
- """Process voice input and update chatbot"""
- if audio_file is None:
- return history, "Please record or upload audio first."
-
- transcribed_text = speech_to_text(audio_file)
-
- if not transcribed_text or transcribed_text.startswith("Could not") or transcribed_text.startswith("No audio"):
- return history, transcribed_text
-
- new_history, _ = legal_chatbot(transcribed_text, history, language)
- return new_history, ""
-
-def read_last_assistant_message(history, language):
- """Find the last assistant message and convert to speech"""
- if not history:
- return None
-
- for message in reversed(history):
- if isinstance(message, dict) and message.get("role") == "assistant":
- content = message.get("content", "")
- if content and content.strip():
- return text_to_speech(content, language)
- return None
-
-
-# ─── PDF REPORT GENERATION ────────────────────────────────────
-def generate_professional_pdf(complaint_id, date_str, name, cnic, phone, city, location, issue_type,
- language, severity, status, reason, confidence, action,
- description, local_message, legal_info_data):
- """Generate a professional PDF complaint report"""
- try:
- from reportlab.lib.pagesizes import A4
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
- from reportlab.lib.units import cm
- from reportlab.lib import colors
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
- from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
-
- buffer = io.BytesIO()
- doc = SimpleDocTemplate(
- buffer,
- pagesize=A4,
- rightMargin=2*cm,
- leftMargin=2*cm,
- topMargin=2*cm,
- bottomMargin=2*cm
- )
+# ─── LAW REFERENCE ────────────────────────────────────────────
+def law_info(issue, language):
+ clean = issue.split(" ", 1)[-1]
+ kb = LEGAL_KB.get(clean, {})
+ local = LOCALIZED.get(clean, {}).get(language, "")
+ rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights", []))
+ out = f"## قانونی حوالہ: {issue}\n\n"
+ out += "### متعلقہ قوانین\n"
+ for law in kb.get("laws", []):
+ out += f" * {law}\n"
+ out += (f"\n### جرمانہ / سزا\n{kb.get('fine','N/A')}\n"
+ f"\n### ذمہ دار ادارہ\n{kb.get('authority','N/A')}\n"
+ f"\n### ہیلپ لائن\n**{kb.get('hotline','N/A')}**\n"
+ f"\n### لازمی جواب کا وقت\n{kb.get('response','N/A')}\n"
+ f"\n### شہری کے حقوق\n{rights}\n"
+ f"\n### ایسکیلیشن\n{kb.get('escalation','N/A')}\n")
+ if local:
+ out += f"\n---\n### مقامی پیغام ({language})\n> {local}\n"
+ out += f"\n---\n*ماخذ: {kb.get('dataset_ref','Pakistani civic law databases')}*"
+ return out
- # Color definitions
- primary_color = colors.HexColor('#0d2b1e')
- secondary_color = colors.HexColor('#1f7a52')
- accent_color = colors.HexColor('#d4870e')
- light_bg = colors.HexColor('#f4f8f5')
- border_color = colors.HexColor('#b8d9c5')
-
- styles = getSampleStyleSheet()
-
- # Custom styles
- title_style = ParagraphStyle(
- 'CustomTitle',
- parent=styles['Heading1'],
- fontSize=18,
- fontName='Helvetica-Bold',
- alignment=TA_CENTER,
- textColor=primary_color,
- spaceAfter=12,
- spaceBefore=6
- )
-
- subtitle_style = ParagraphStyle(
- 'CustomSubtitle',
- parent=styles['Normal'],
- fontSize=11,
- fontName='Helvetica',
- alignment=TA_CENTER,
- textColor=secondary_color,
- spaceAfter=20
- )
-
- section_style = ParagraphStyle(
- 'Section',
- parent=styles['Heading2'],
- fontSize=13,
- fontName='Helvetica-Bold',
- textColor=secondary_color,
- spaceBefore=12,
- spaceAfter=6
- )
-
- body_style = ParagraphStyle(
- 'Body',
- parent=styles['Normal'],
- fontSize=9,
- fontName='Helvetica',
- leading=14,
- spaceAfter=6
- )
-
- story = []
-
- # Header
- story.append(Paragraph("GOVERNMENT OF PAKISTAN", title_style))
- story.append(Paragraph("CIVIC COMPLAINT REPORT", subtitle_style))
- story.append(HRFlowable(width="100%", thickness=2, color=secondary_color))
- story.append(Spacer(1, 0.3*cm))
-
- # Complaint Reference Box
- severity_icon = "🟢" if severity <= 3 else ("🟡" if severity <= 6 else ("🟠" if severity <= 8 else "🔴"))
-
- ref_data = [
- ["Complaint Reference:", complaint_id, "Date of Filing:", date_str],
- ["Issue Type:", issue_type, "Severity:", f"{severity_icon} {severity}/10"],
- ["Language:", language, "Status:", status],
- ]
- ref_table = Table(ref_data, colWidths=[4*cm, 4.5*cm, 3.5*cm, 5*cm])
- ref_table.setStyle(TableStyle([
- ('BACKGROUND', (0, 0), (0, -1), light_bg),
- ('BACKGROUND', (2, 0), (2, -1), light_bg),
- ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
- ('FONTNAME', (2, 0), (2, -1), 'Helvetica-Bold'),
- ('FONTSIZE', (0, 0), (-1, -1), 9),
- ('GRID', (0, 0), (-1, -1), 0.5, border_color),
- ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
- ('TOPPADDING', (0, 0), (-1, -1), 6),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
- ]))
- story.append(ref_table)
- story.append(Spacer(1, 0.3*cm))
-
- # Section A - Complainant Information
- story.append(Paragraph("SECTION A: COMPLAINANT INFORMATION", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- complainant_data = [
- [f"Full Name:", name, f"CNIC:", cnic or "Not Provided"],
- [f"Phone:", phone or "Not Provided", f"City:", city],
- [f"Location:", location, "", ""],
- ]
- complainant_table = Table(complainant_data, colWidths=[3.5*cm, 5*cm, 3*cm, 5.5*cm])
- complainant_table.setStyle(TableStyle([
- ('FONTSIZE', (0, 0), (-1, -1), 9),
- ('TOPPADDING', (0, 0), (-1, -1), 5),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
- ('LEFTPADDING', (0, 0), (-1, -1), 4),
- ]))
- story.append(complainant_table)
- story.append(Spacer(1, 0.2*cm))
-
- # Section B - Complaint Details
- story.append(Paragraph("SECTION B: COMPLAINT DETAILS", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- if description and description.strip():
- story.append(Paragraph(f"Description: {description.strip()}", body_style))
- story.append(Spacer(1, 0.2*cm))
-
- # Section C - Verification Results
- story.append(Paragraph("SECTION C: VERIFICATION RESULTS", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- verification_data = [
- [f"Status:", status, f"Confidence:", confidence],
- [f"Finding:", (reason[:250] + "...") if len(reason) > 250 else reason, "", ""],
- [f"Recommended Action:", (action[:250] + "...") if len(action) > 250 else action, "", ""],
- ]
- verification_table = Table(verification_data, colWidths=[3.5*cm, 13.5*cm])
- verification_table.setStyle(TableStyle([
- ('FONTSIZE', (0, 0), (-1, -1), 9),
- ('TOPPADDING', (0, 0), (-1, -1), 6),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
- ('VALIGN', (0, 0), (-1, -1), 'TOP'),
- ]))
- story.append(verification_table)
- story.append(Spacer(1, 0.2*cm))
-
- # Section D - Legal Framework
- story.append(Paragraph("SECTION D: LEGAL FRAMEWORK", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- legal_text = f"Applicable Laws:
"
- for law in legal_info_data.get("laws", []):
- legal_text += f"• {law}
"
- legal_text += f"
Responsible Authority: {legal_info_data.get('authority', 'N/A')}
"
- legal_text += f"Official Helpline: {legal_info_data.get('hotline', 'N/A')}
"
- legal_text += f"Statutory Response Time: {legal_info_data.get('response', 'N/A')}
"
- legal_text += f"Fine / Penalty: {legal_info_data.get('fine', 'N/A')}"
-
- story.append(Paragraph(legal_text, body_style))
- story.append(Spacer(1, 0.2*cm))
-
- # Section E - Citizen Rights
- story.append(Paragraph("SECTION E: CITIZEN'S LEGAL RIGHTS", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- rights_text = ""
- for right in legal_info_data.get("citizen_rights", []):
- rights_text += f"• {right}
"
- rights_text += f"
Escalation Path: {legal_info_data.get('escalation', 'CM Portal: 0800-02345')}"
-
- story.append(Paragraph(rights_text, body_style))
- story.append(Spacer(1, 0.2*cm))
-
- # Section F - Localized Notice
- if local_message:
- story.append(Paragraph(f"SECTION F: NOTICE IN {language.upper()}", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
- story.append(Paragraph(local_message, body_style))
- story.append(Spacer(1, 0.2*cm))
-
- # Section G - Action Directive
- story.append(Paragraph("SECTION G: ACTION DIRECTIVE", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
-
- action_text = f"""
- MANDATORY ACTION REQUIRED WITHIN: {legal_info_data.get('response', '72 hours').upper()}
- Authority: {legal_info_data.get('authority', 'N/A')}
- Helpline: {legal_info_data.get('hotline', 'N/A')}
- Citizen Portal: citizenportal.gov.pk | CM Helpline: 0800-02345
- """
- story.append(Paragraph(action_text, body_style))
- story.append(Spacer(1, 0.2*cm))
-
- # Declaration
- story.append(Paragraph("DECLARATION", section_style))
- story.append(HRFlowable(width="100%", thickness=0.5, color=border_color))
- story.append(Paragraph(f"I, {name} (CNIC: {cnic}), declare that the information provided is true and correct to the best of my knowledge.", body_style))
- story.append(Spacer(1, 0.3*cm))
-
- sig_data = [
- [f"Signature: ________________________", f"Date: {date_str}", f"Ref: {complaint_id}"],
- ]
- sig_table = Table(sig_data, colWidths=[6*cm, 5*cm, 6*cm])
- sig_table.setStyle(TableStyle([
- ('FONTSIZE', (0, 0), (-1, -1), 8),
- ('TOPPADDING', (0, 0), (-1, -1), 8),
- ]))
- story.append(sig_table)
-
- # Footer
- story.append(Spacer(1, 0.5*cm))
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.gray))
- story.append(Paragraph(
- f"Generated by Rahbar — Pakistan's Civic Complaint System | Complaint ID: {complaint_id}",
- ParagraphStyle('Footer', parent=styles['Normal'], fontSize=7, textColor=colors.gray, alignment=TA_CENTER)
- ))
-
- doc.build(story)
- buffer.seek(0)
-
- pdf_path = f"/tmp/Rahbar_Report_{complaint_id}.pdf"
- with open(pdf_path, "wb") as f:
- f.write(buffer.getvalue())
- return pdf_path
+# ─── WHATSAPP ─────────────────────────────────────────────────
+def make_whatsapp_link(text):
+ return f"https://wa.me/?text={urllib.parse.quote(text[:1000])}"
- except Exception as e:
- print(f"PDF generation error: {e}")
- # Fallback to text file
- pdf_path = f"/tmp/Rahbar_Report_{complaint_id}.txt"
- with open(pdf_path, "w", encoding="utf-8") as f:
- f.write(f"RAHBAR COMPLAINT REPORT\n")
- f.write(f"=" * 50 + "\n")
- f.write(f"ID: {complaint_id}\n")
- f.write(f"Date: {date_str}\n")
- f.write(f"Issue: {issue_type}\n")
- f.write(f"Location: {location}, {city}\n")
- f.write(f"Severity: {severity}/10\n")
- f.write(f"Name: {name}\n")
- f.write(f"CNIC: {cnic}\n")
- f.write(f"Status: {status}\n")
- f.write(f"Recommended Action: {action}\n")
- return pdf_path
+# ─── ADMIN STATS ──────────────────────────────────────────────
+def get_admin_stats():
+ total = len(complaint_log)
+ if total == 0:
+ return "ابھی تک کوئی شکایت درج نہیں ہوئی۔", ""
+ counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
+ cities = {}
+ severities = []
+ for c in complaint_log:
+ issue = c.get("issue","").split(" ",1)[-1]
+ counts[issue] = counts.get(issue, 0) + 1
+ city = c.get("city","Unknown")
+ cities[city] = cities.get(city, 0) + 1
+ severities.append(c.get("severity", 5))
+ avg_sev = sum(severities)/len(severities) if severities else 0
+ top_city = max(cities, key=cities.get) if cities else "N/A"
+ stats_md = f"""## ایڈمن ڈیش بورڈ
+| معیار | تعداد |
+|--------|-------|
+| کل شکایات | **{total}** |
+| اوسط شدت | **{avg_sev:.1f}/10** |
+| سب سے زیادہ شہر | **{top_city}** |
+### مسئلے کی قسم کے مطابق
+| مسئلہ | تعداد |
+|-------|-------|
+| کچرا | {counts['Garbage']} |
+| سڑک گڑھا | {counts['Pot Hole']} |
+| پائپ لیکیج | {counts['Pipe Leakage']} |
+
+### شہر کے مطابق
+"""
+ for city, cnt in sorted(cities.items(), key=lambda x: -x[1]):
+ stats_md += f"| {city} | {cnt} |\n"
+ log_md = "## حالیہ شکایات\n\n"
+ for c in reversed(complaint_log[-10:]):
+ log_md += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | "
+ f"{c['issue']} | شدت {c['severity']}/10 | "
+ f"شہری: {c.get('name','N/A')} (CNIC: {c.get('cnic','N/A')})\n\n")
+ return stats_md, log_md
+
+def severity_icon(score):
+ if score <= 3: return "سبز"
+ if score <= 6: return "زرد"
+ if score <= 8: return "نارنجی"
+ return "سرخ"
+
+def _ubold(text):
+ result = []
+ for ch in str(text):
+ o = ord(ch)
+ if 0x41 <= o <= 0x5A:
+ result.append(chr(0x1D400 + o - 0x41))
+ elif 0x61 <= o <= 0x7A:
+ result.append(chr(0x1D41A + o - 0x61))
+ elif 0x30 <= o <= 0x39:
+ result.append(chr(0x1D7CE + o - 0x30))
+ else:
+ result.append(ch)
+ return "".join(result)
+
+def build_professional_report(complaint_id, timestamp, name, cnic, phone, city, location,
+ issue_type, language, severity, sev_icon,
+ yolo_summary, gemini_status, gemini_reason,
+ gemini_confidence, gemini_action, kb, description, local_msg):
+ date_str = datetime.datetime.now().strftime("%d %B %Y")
+ time_str = datetime.datetime.now().strftime("%I:%M %p")
+
+ laws_text = ""
+ for law in kb.get("laws", []):
+ laws_text += f"\n - {law}"
+
+ rights_text = ""
+ for right in kb.get("citizen_rights", []):
+ rights_text += f"\n - {right}"
+
+ def H(title): return f"\n {_ubold(title)}\n {'─' * len(title)}"
+
+ report = f"""{_ubold('GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT')}
+ {_ubold('Rahbar Digital Civic Redressal System — رہبر')}
+
+ شکایت نمبر : {complaint_id}
+ تاریخ : {date_str} وقت: {time_str}
+ طریقہ : Rahbar Digital Platform (AI-Verified)
+ زبان : {language}
+{H('SECTION A — COMPLAINANT / شاکی کی معلومات')}
+ مکمل نام : {name}
+ شناختی کارڈ : {cnic}
+ رابطہ نمبر : {phone if phone else "فراہم نہیں کیا"}
+ شہر : {city}
+ علاقہ : {location}
+{H('SECTION B — COMPLAINT DETAILS / شکایت کی تفصیل')}
+ نوعیت : {issue_type}
+ مقام : {location}, {city}
+ تاریخ/وقت : {date_str}, {time_str}
+ شدت : [{sev_icon}] {severity} / 10
+
+ تفصیل:
+ {description.strip() if description.strip() else "[اضافی تفصیل فراہم نہیں کی گئی]"}
+{H('SECTION C — AI VERIFICATION / AI تصدیق')}
+ حیثیت : {gemini_status}
+ اعتماد : {gemini_confidence}
+ نتیجہ : {gemini_reason}
+ کارروائی : {gemini_action if gemini_action else "فوری فیلڈ معائنہ ضروری ہے۔"}
+ YOLO : {yolo_summary}
+{H('SECTION D — LEGAL FRAMEWORK / قانونی بنیاد')}
+ متعلقہ قوانین:{laws_text}
+
+ ذمہ دار ادارہ : {kb.get('authority','N/A')}
+ سرکاری ہیلپ لائن: {kb.get('hotline','N/A')}
+ قانونی جواب : {kb.get('response','N/A')}
+ جرمانہ : {kb.get('fine','N/A')}
+{H("SECTION E — CITIZEN'S RIGHTS / شہری کے حقوق")}{rights_text}
+
+ ایسکیلیشن : {kb.get('escalation','CM Portal: 0800-02345')}
+{H(f'SECTION F — LOCALIZED NOTICE / مقامی نوٹس ({language})')}
+ {local_msg}
+{H('SECTION G — ACTION DIRECTIVE / کارروائی کی ہدایت')}
+ {_ubold('MANDATORY ACTION REQUIRED WITHIN:')} {kb.get('response','72 hours').upper()}
+
+ ذمہ دار : {kb.get('authority','Local Authority')}
+ ہیلپ لائن : {kb.get('hotline','N/A')}
+ پورٹل : citizenportal.gov.pk | CM: 0800-02345
+{H('DECLARATION / اعلامیہ')}
+ میں، {name} (CNIC: {cnic})، اعلان کرتا/کرتی ہوں کہ فراہم کردہ
+ معلومات میری بہترین معلومات کے مطابق درست ہیں۔
+
+ دستخط: ________________________ (ڈیجیٹل) تاریخ: {date_str}
+ حوالہ: {complaint_id}
+{H('FOR OFFICIAL USE ONLY / سرکاری استعمال')}
+ وصول کنندہ : ___________________________
+ وصول کی تاریخ : ___________________________
+ کارروائی : ___________________________
+ حل کی تاریخ : ___________________________
+
+ Generated by: Rahbar — Pakistan's AI Civic Redressal Platform
+ Timestamp: {timestamp}
+"""
+ return report
# ─── MAIN REPORT FUNCTION ─────────────────────────────────────
def make_report(image, issue_type, city, location, name, cnic, phone,
description, language, enable_tts):
-
if image is None:
- return (None, "Please upload an image.", "", "", None, "", None, None)
- if not location or not location.strip():
- return (None, "Please enter a location.", "", "", None, "", None, None)
- if not name or not name.strip():
- return (None, "Please enter your full name.", "", "", None, "", None, None)
- if not cnic or not cnic.strip():
- return (None, "Please enter your CNIC number.", "", "", None, "", None, None)
+ return (None, "براہ کرم تصویر اپلوڈ کریں۔", "", "", None, "", None)
+ if not location.strip():
+ return (None, "براہ کرم مقام درج کریں۔", "", "", None, "", None)
+ if not name.strip():
+ return (None, "براہ کرم اپنا مکمل نام درج کریں۔", "", "", None, "", None)
+ if not cnic.strip():
+ return (None, "براہ کرم اپنا شناختی کارڈ نمبر درج کریں۔", "", "", None, "", None)
complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
- timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- date_str = datetime.datetime.now().strftime("%d %B %Y")
- issue_clean = issue_type.split(" ", 1)[-1]
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ clean_issue = issue_type.split(" ", 1)[-1]
+
+ annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type)
+ gemini_raw = analyze_with_gemini(image, issue_type, location, city, yolo_summary)
+ gemini_parsed = parse_gemini_response(gemini_raw)
+ gemini_status = gemini_parsed["status"]
+ gemini_reason = gemini_parsed["reason"]
+ gemini_severity = gemini_parsed["severity"]
- # Analyze image
- annotated_img, yolo_summary, severity, status, reason, reason_urdu, confidence, action = analyze_image(image, issue_type)
-
- if status == "REJECTED":
+ if gemini_status == "REJECTED":
return (annotated_img,
- f"**COMPLAINT REJECTED**\n\nReason: {reason}\nConfidence: {confidence}\n\nPlease upload a clear image showing the reported issue ({issue_type}).",
- "", "", None, complaint_id, None, None)
-
- # Get legal information
- legal_info_data = LEGAL_INFO.get(issue_clean, LEGAL_INFO.get("Garbage", {}))
- local_message = LOCALIZED.get(issue_clean, {}).get(language, "")
- severity_icon = "🟢" if severity <= 3 else ("🟡" if severity <= 6 else ("🟠" if severity <= 8 else "🔴"))
-
- # Get legal advice
- legal_advice = get_legal_advice(issue_type, location, city, severity, language)
-
- # Build report
- report = f"""======================================================================
- GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT
- Rahbar Digital Civic Redressal System | رہبر
-======================================================================
- Complaint ID : {complaint_id}
- Date / Time : {timestamp}
- Language : {language}
-======================================================================
- SECTION A — COMPLAINANT INFORMATION
-======================================================================
- Full Name : {name}
- CNIC : {cnic}
- Phone : {phone or "Not Provided"}
- City : {city}
- Location : {location}
-======================================================================
- SECTION B — COMPLAINT DETAILS
-======================================================================
- Issue : {issue_type}
- Severity : {severity_icon} {severity} / 10
- Description : {description.strip() if description else "[None provided]"}
-======================================================================
- SECTION C — VERIFICATION RESULTS
-======================================================================
- Status : {status}
- Confidence : {confidence}
- Finding : {reason}
- Action : {action}
-======================================================================
- SECTION D — LEGAL FRAMEWORK
-======================================================================
- Laws : {', '.join(legal_info_data.get('laws', []))}
- Authority : {legal_info_data.get('authority', 'N/A')}
- Helpline : {legal_info_data.get('hotline', 'N/A')}
- Response : {legal_info_data.get('response', 'N/A')}
- Fine : {legal_info_data.get('fine', 'N/A')}
-======================================================================
- SECTION E — YOUR RIGHTS
-======================================================================
- {chr(10).join(f' • {r}' for r in legal_info_data.get('citizen_rights', []))}
-
- Escalation : {legal_info_data.get('escalation', 'CM Portal: 0800-02345')}
-======================================================================
- SECTION F — NOTICE ({language})
-======================================================================
- {local_message}
-======================================================================
- SECTION G — ACTION DIRECTIVE
-======================================================================
- MANDATORY ACTION WITHIN: {legal_info_data.get('response', '72 hours').upper()}
- Authority : {legal_info_data.get('authority', 'N/A')}
- Helpline : {legal_info_data.get('hotline', 'N/A')}
- Portal : citizenportal.gov.pk | CM: 0800-02345
-======================================================================
- DECLARATION
-======================================================================
- I, {name} (CNIC: {cnic}), declare this information is true.
- Signature: ______________________ Date: {date_str}
-======================================================================
- Generated by Rahbar — Pakistan's Civic Complaint System
- Complaint ID: {complaint_id}
-======================================================================
-"""
+ f"شکایت AI تصدیق میں مسترد\n\nوجہ: {gemini_reason}\n"
+ f"اعتماد: {gemini_parsed.get('confidence','N/A')}\n\n"
+ f"براہ کرم مسئلے کی واضح تصویر اپلوڈ کریں ({issue_type})۔\n"
+ "آپ کی شکایت محفوظ نہیں ہوئی۔",
+ "", "", None, complaint_id, None)
+
+ if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
+ gemini_reason = "Gemini API key نہیں ہے — تصدیق چھوڑ دی گئی۔"
+ gemini_status = "APPROVED_WITH_WARNING"
+
+ final_severity = gemini_severity if gemini_status == "APPROVED" else yolo_severity
+ kb = LEGAL_KB.get(clean_issue, {})
+ local = LOCALIZED.get(clean_issue, {}).get(language, "")
+ sev_icon = severity_icon(final_severity)
+
+ llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
+
+ report = build_professional_report(
+ complaint_id, timestamp, name, cnic, phone, city, location,
+ issue_type, language, final_severity, sev_icon,
+ yolo_summary, gemini_status, gemini_reason,
+ gemini_parsed.get("confidence","N/A"), gemini_parsed.get("action",""),
+ kb, description, local
+ )
- # Log complaint
complaint_log.append({
"id": complaint_id, "timestamp": timestamp, "city": city, "location": location,
- "issue": issue_type, "severity": severity, "language": language,
- "name": name, "cnic": cnic, "phone": phone
+ "issue": issue_type, "severity": final_severity, "language": language,
+ "name": name, "cnic": cnic, "phone": phone,
})
- # WhatsApp share text
- wa_text = f"Rahbar Complaint\nRef: {complaint_id}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {severity}/10\nAuthority: {legal_info_data.get('authority', 'N/A')}\nHelpline: {legal_info_data.get('hotline', 'N/A')}\nFiled: {timestamp}"
- wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})"
-
- # Audio outputs
- report_tts = text_to_speech(report[:800], language) if enable_tts else None
- advice_tts = text_to_speech(legal_advice[:600], language) if enable_tts else None
-
- # Generate PDF
- pdf_path = generate_professional_pdf(
- complaint_id, date_str, name, cnic, phone, city, location, issue_type,
- language, severity, status, reason, confidence, action,
- description or "", local_message, legal_info_data
- )
-
- return (annotated_img, report, wa_md, legal_advice, report_tts, complaint_id, advice_tts, pdf_path)
-
-
-def update_areas(city):
- areas = CITIES_AREAS.get(city, ["Enter area manually"])
- return gr.Dropdown(choices=areas, value=areas[0])
-
-
-def law_info(issue, language):
- issue_clean = issue.split(" ", 1)[-1]
- info = LEGAL_INFO.get(issue_clean, LEGAL_INFO.get("Garbage", {}))
- local = LOCALIZED.get(issue_clean, {}).get(language, "")
- rights = "\n".join(f" * {r}" for r in info.get("citizen_rights", []))
-
- out = f"""## Legal Reference: {issue}
-
-### Applicable Laws
-{chr(10).join(f' * {l}' for l in info.get('laws', []))}
-
-### Fine / Penalty
-{info.get('fine', 'N/A')}
-
-### Responsible Authority
-{info.get('authority', 'N/A')}
-
-### Emergency Helpline
-**{info.get('hotline', 'N/A')}**
-
-### Required Government Response Time
-{info.get('response', 'N/A')}
-
-### Your Rights
-{rights}
-
-### If Ignored — Escalate To
-{info.get('escalation', 'N/A')}
-
----
-### Notice in {language}
-> {local}
-"""
- return out
-
-
-def get_admin_stats():
- total = len(complaint_log)
- if not total:
- return "No complaints filed yet.", ""
-
- counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
- cities = {}
- sevs = []
-
- for c in complaint_log:
- iss = c.get("issue", "").split(" ", 1)[-1]
- counts[iss] = counts.get(iss, 0) + 1
- cit = c.get("city", "?")
- cities[cit] = cities.get(cit, 0) + 1
- sevs.append(c.get("severity", 5))
-
- avg_sev = sum(sevs) / len(sevs) if sevs else 0
- top_city = max(cities, key=cities.get) if cities else "N/A"
-
- stats = f"""## Complaint Statistics
+ wa_text = (f"رہبر شکایت\nنمبر: {complaint_id}\nمسئلہ: {issue_type}\n"
+ f"مقام: {location}, {city}\nشدت: {final_severity}/10\n"
+ f"ادارہ: {kb.get('authority','N/A')}\nہیلپ لائن: {kb.get('hotline','N/A')}\n"
+ f"وقت: {timestamp}")
+ wa_md = f"[واٹس ایپ پر شیئر کریں]({make_whatsapp_link(wa_text)})"
-| Metric | Value |
-|--------|-------|
-| **Total Complaints** | {total} |
-| **Average Severity** | {avg_sev:.1f}/10 |
-| **Most Active City** | {top_city} |
+ report_tts_path = None
+ if enable_tts:
+ tts_text = (f"شکایت {complaint_id} درج ہو گئی۔ "
+ f"مسئلہ: {issue_type}، مقام: {location}، شہر: {city}۔ "
+ f"شدت: {final_severity} میں سے {final_severity}۔ {local}")
+ report_tts_path = make_tts(tts_text, language)
-### By Issue Type
-| Issue | Count |
-|-------|-------|
-"""
- for k, v in counts.items():
- stats += f"| {k} | {v} |\n"
-
- stats += "\n### By City\n| City | Count |\n|------|-------|\n"
- for c, n in sorted(cities.items(), key=lambda x: -x[1]):
- stats += f"| {c} | {n} |\n"
-
- log = "## Recent Complaints\n\n"
- for c in reversed(complaint_log[-10:]):
- log += f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | {c['issue']} | Severity {c['severity']}/10 | {c.get('name', '?')}\n\n"
-
- return stats, log
+ advice_tts_path = make_tts(llama_advice[:600], language)
+ return annotated_img, report, wa_md, llama_advice, report_tts_path, complaint_id, advice_tts_path
-# ─── CSS — Light and Dark Mode ────────────────────────────────
+# ─── CSS ──────────────────────────────────────────────────────
CSS = """
-@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap');
:root {
- --primary: #0d2b1e;
- --primary-light: #1f7a52;
- --accent: #d4870e;
- --surface: #ffffff;
- --surface-light: #f4f8f5;
- --text: #0d2b1e;
- --text-light: #5a8a6e;
- --border: #b8d9c5;
+ --g9:#0d2b1e; --g8:#14432e; --g7:#1a5c3f; --g6:#1f7a52;
+ --g5:#25a06b; --g4:#2ec97f;
+ --a5:#f5a623; --a4:#d4870e; --a3:#b8730c;
+ --surface:#ffffff; --surface2:#f4f8f5; --surface3:#e8f3ed;
+ --text-primary:#0d2b1e; --text-secondary:#2d5a3e; --text-muted:#5a8a6e;
+ --border:#b8d9c5; --border-strong:#1f7a52;
+ --shadow:0 2px 8px rgba(13,43,30,.10);
+ --radius:12px; --radius-lg:20px;
+ --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#0a1f14 100%);
+ --badge-bg:#ffffff; --badge-border:#1a5c3f;
+ --warn-bg:#fffbf0; --warn-border:rgba(245,166,35,.5);
+ --info-bg:#f0faf4; --info-border:#1f7a52;
}
@media (prefers-color-scheme: dark) {
:root {
- --primary: #d0f0df;
- --primary-light: #30aa72;
- --accent: #f5a623;
- --surface: #0f1f16;
- --surface-light: #162b1e;
- --text: #d0f0df;
- --text-light: #8fcfae;
- --border: #2a5c3e;
+ --g9:#d0f0df; --g8:#1e6644; --g7:#28885a; --g6:#30aa72;
+ --g5:#3dcf8a; --g4:#5de3a3;
+ --a5:#f7bc57; --a4:#f5a623; --a3:#f0921a;
+ --surface:#0f1f16; --surface2:#162b1e; --surface3:#1c3828;
+ --text-primary:#d0f0df; --text-secondary:#8fcfae; --text-muted:#5a9a78;
+ --border:#2a5c3e; --border-strong:#30aa72;
+ --shadow:0 2px 12px rgba(0,0,0,.4);
+ --header-bg:linear-gradient(135deg,#0a1f14 0%,#071510 60%,#050f0a 100%);
+ --badge-bg:#0f1f16; --badge-border:#28885a;
+ --warn-bg:#1e1800; --warn-border:rgba(247,188,87,.4);
+ --info-bg:#0e1f16; --info-border:#30aa72;
}
}
-* {
- font-family: 'DM Sans', sans-serif !important;
-}
+*, *::before, *::after { box-sizing: border-box; }
-.gradio-container {
+body, .gradio-container {
+ font-family: 'DM Sans', sans-serif !important;
background: var(--surface) !important;
+ color: var(--text-primary) !important;
}
-.gr-box, .gr-form, .gr-panel {
- background: var(--surface) !important;
- border-color: var(--border) !important;
+.saaf-header {
+ background: var(--header-bg);
+ border-bottom: 2px solid var(--g6);
+ padding: 24px 20px 20px;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
}
-
-label, .gr-label {
- color: var(--text) !important;
+.saaf-header::before {
+ content: '';
+ position: absolute; inset: 0;
+ background: radial-gradient(ellipse 60% 60% at 50% 0%, rgba(37,160,107,.18), transparent);
+ pointer-events: none;
}
-
-input, textarea, select {
- background: var(--surface) !important;
- border: 1px solid var(--border) !important;
- color: var(--text) !important;
+.saaf-header h1 {
+ font-family: 'Playfair Display', serif !important;
+ font-size: clamp(1.8rem, 5vw, 3rem) !important;
+ font-weight: 900 !important;
+ color: #fafdf8 !important;
+ margin: 0 0 4px !important;
+ line-height: 1.1;
}
-
-button.primary {
- background: linear-gradient(135deg, var(--primary-light), #25a06b) !important;
- color: white !important;
- border: none !important;
- font-weight: 600 !important;
+.saaf-header .urdu-title {
+ font-family: 'Noto Nastaliq Urdu', serif;
+ font-size: clamp(1rem, 3vw, 1.6rem);
+ color: #f7bc57;
+ direction: rtl;
+ margin: 4px 0 8px;
+}
+.saaf-header .tagline {
+ font-size: clamp(.75rem, 2vw, .95rem);
+ color: #5de3a3;
+ letter-spacing: .08em;
+ text-transform: uppercase;
}
-button.primary:hover {
- transform: translateY(-1px) !important;
+.badge-strip {
+ display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
+ padding: 10px 16px;
+ background: var(--surface2);
+ border-bottom: 1px solid var(--border);
}
+.badge {
+ font-size: .72rem; font-weight: 600; letter-spacing: .06em;
+ padding: 4px 12px; border-radius: 20px; text-transform: uppercase;
+}
+.badge-ai { background: var(--badge-bg); color: var(--g4); border: 1px solid var(--badge-border); }
+.badge-pk { background: var(--badge-bg); color: var(--a4); border: 1px solid var(--warn-border); }
+.badge-live{ background: var(--badge-bg); color: #ff8080; border: 1px solid rgba(232,83,83,.4); }
-.gr-markdown h1, .gr-markdown h2, .gr-markdown h3 {
- color: var(--primary-light) !important;
+.tab-nav { background: var(--surface2) !important; border-bottom: 2px solid var(--border) !important; }
+.tab-nav button {
+ font-family: 'DM Sans', sans-serif !important;
+ font-weight: 500 !important; font-size: .85rem !important;
+ color: var(--text-muted) !important;
+ padding: 12px 18px !important; border-radius: 0 !important;
+ transition: all .2s !important;
+}
+.tab-nav button.selected,
+.tab-nav button[aria-selected="true"] {
+ color: var(--a4) !important;
+ border-bottom: 3px solid var(--a5) !important;
+ background: transparent !important;
}
-.message {
- border-radius: 12px !important;
+.card-title {
+ font-size: .7rem; font-weight: 600; letter-spacing: .1em;
+ text-transform: uppercase; color: var(--g4);
+ margin-bottom: 12px; padding-bottom: 8px;
+ border-bottom: 1px solid var(--border);
}
-.message.user {
- background: var(--surface-light) !important;
+label, .gradio-container .label-wrap span {
+ color: var(--text-primary) !important;
}
-.message.bot {
+.gradio-container input,
+.gradio-container textarea {
background: var(--surface) !important;
- border: 1px solid var(--border) !important;
+ border: 1px solid var(--border-strong) !important;
+ border-radius: var(--radius) !important;
+ color: var(--text-primary) !important;
+ font-family: 'DM Sans', sans-serif !important;
}
-
-.header-title {
- text-align: center;
- padding: 20px;
- background: linear-gradient(135deg, #0d2b1e, #1a5c3f);
- color: white;
- border-radius: 12px 12px 0 0;
+.gradio-container input:focus,
+.gradio-container textarea:focus {
+ border-color: var(--a5) !important;
+ outline: none !important;
+ box-shadow: 0 0 0 3px rgba(245,166,35,.15) !important;
}
-
-.header-title h1 {
- font-size: 2rem;
- margin: 0;
- font-weight: bold;
+.gradio-container .wrap { background: var(--surface) !important; border-color: var(--border-strong) !important; }
+.gradio-container .block { background: var(--surface) !important; }
+
+.gradio-container button.primary {
+ background: linear-gradient(135deg, var(--g6), var(--g5)) !important;
+ color: #fafdf8 !important;
+ border: none !important; border-radius: var(--radius) !important;
+ font-weight: 600 !important; font-size: .9rem !important;
+ padding: 12px 24px !important; cursor: pointer !important;
+ box-shadow: var(--shadow) !important; transition: all .2s !important;
+}
+.gradio-container button.primary:hover {
+ background: linear-gradient(135deg, var(--g5), var(--g4)) !important;
+ transform: translateY(-1px) !important;
+}
+.gradio-container button.secondary {
+ background: var(--surface) !important;
+ border: 1px solid var(--border-strong) !important;
+ color: var(--g4) !important;
}
-.header-title p {
- margin: 5px 0 0;
- opacity: 0.9;
+.gradio-container [data-testid="image"] {
+ border: 2px dashed var(--border-strong) !important;
+ border-radius: var(--radius-lg) !important;
+ background: var(--surface2) !important;
}
+.gradio-container .prose h2,
+.gradio-container .prose h3 { color: var(--a4) !important; }
+
+.gradio-container audio { width: 100% !important; border-radius: var(--radius) !important; }
+
.info-box {
- background: var(--surface-light);
- border-left: 4px solid var(--primary-light);
- padding: 10px 15px;
- border-radius: 8px;
- margin: 10px 0;
- font-size: 0.9rem;
+ background: var(--info-bg);
+ border: 1px solid var(--info-border);
+ border-left: 4px solid var(--g5);
+ border-radius: var(--radius);
+ padding: 12px 16px;
+ font-size: .88rem; line-height: 1.6;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ direction: rtl;
+ text-align: right;
}
-
.warn-box {
- background: #fffbf0;
- border-left: 4px solid var(--accent);
- padding: 10px 15px;
- border-radius: 8px;
- margin: 10px 0;
- font-size: 0.9rem;
-}
-
-@media (prefers-color-scheme: dark) {
- .warn-box {
- background: #1e1800;
- }
+ background: var(--warn-bg);
+ border: 1px solid var(--warn-border);
+ border-left: 4px solid var(--a5);
+ border-radius: var(--radius);
+ padding: 12px 16px;
+ font-size: .88rem;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ direction: rtl;
+ text-align: right;
}
.hotline-pill {
display: inline-block;
- background: var(--surface-light);
- padding: 2px 10px;
- border-radius: 20px;
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--accent);
- margin: 0 2px;
+ background: var(--surface2); color: var(--a4);
+ border: 1px solid var(--warn-border);
+ border-radius: 20px; padding: 2px 12px;
+ font-size: .8rem; font-weight: 600;
+}
+
+.gradio-container textarea {
+ font-family: 'DM Mono', 'Courier New', monospace !important;
+ font-size: .82rem !important;
+ line-height: 1.7 !important;
+}
+
+.gradio-container select,
+.gradio-container [data-testid="dropdown"] {
+ background: var(--surface) !important;
+ color: var(--text-primary) !important;
+ border-color: var(--border-strong) !important;
+}
+
+.gradio-container .message.user { background: var(--surface3) !important; color: var(--text-primary) !important; }
+.gradio-container .message.bot { background: var(--surface2) !important; color: var(--text-primary) !important; }
+
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--surface2); }
+::-webkit-scrollbar-thumb { background: var(--g6); border-radius: 3px; }
+
+@media (max-width: 640px) {
+ .saaf-header { padding: 16px 12px; }
+ .tab-nav button { padding: 10px 12px !important; font-size: .78rem !important; }
}
"""
HEADER_HTML = """
-
Pakistan's Civic Complaint System | پاکستان کا سٹیزن شکایات نظام
++ نقشے پر ٹیپ کریں یا پن گھسیٹیں - پتہ خودکار بھرتا ہے +
+