Ayesha Mehar commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
| 3 |
║ Rahbar | رہبر ║
|
| 4 |
║ Pakistan's AI-Powered Civic Complaint System ║
|
| 5 |
╚══════════════════════════════════════════════════════════════╝
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
• Chatbot uses {"role","content"} dict format (Gradio 6+)
|
| 8 |
-
• GPS button works with proper permission flow
|
| 9 |
• Report TTS in user-selected language
|
| 10 |
• Voice input (STT) working
|
| 11 |
• پنجابی language added
|
|
@@ -292,9 +294,6 @@ Keep it concise and practical for a common Pakistani citizen."""
|
|
| 292 |
|
| 293 |
# ─── CHATBOT — Gradio 6+ dict format ─────────────────────────
|
| 294 |
def legal_chatbot(user_message, history, language):
|
| 295 |
-
"""
|
| 296 |
-
Gradio 6 with type='messages': history = list of {"role":..., "content":...} dicts
|
| 297 |
-
"""
|
| 298 |
if history is None:
|
| 299 |
history = []
|
| 300 |
if not user_message.strip():
|
|
@@ -348,7 +347,6 @@ Rules:
|
|
| 348 |
try:
|
| 349 |
from groq import Groq
|
| 350 |
client = Groq(api_key=GROQ_API_KEY)
|
| 351 |
-
# Build Groq API messages
|
| 352 |
api_messages = [{"role": "system", "content": system_content}]
|
| 353 |
for msg in history[-20:]:
|
| 354 |
api_messages.append({"role": msg["role"], "content": msg["content"]})
|
|
@@ -472,36 +470,42 @@ def severity_icon(score):
|
|
| 472 |
if score <= 8: return "ORANGE"
|
| 473 |
return "RED"
|
| 474 |
|
| 475 |
-
# ─── PROFESSIONAL REPORT ──
|
| 476 |
def build_professional_report(complaint_id, timestamp, name, cnic, phone, city, location,
|
| 477 |
issue_type, language, severity, sev_icon,
|
| 478 |
yolo_summary, gemini_status, gemini_reason,
|
| 479 |
gemini_confidence, gemini_action, kb, description, local_msg):
|
| 480 |
date_str = datetime.datetime.now().strftime("%d %B %Y")
|
| 481 |
time_str = datetime.datetime.now().strftime("%I:%M %p")
|
| 482 |
-
|
| 483 |
-
=
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
Complaint Reference No. : {complaint_id}
|
| 489 |
Date of Filing : {date_str} Time: {time_str}
|
| 490 |
Filing Method : Rahbar Digital Platform (AI-Verified)
|
| 491 |
Report Language : {language}
|
| 492 |
|
| 493 |
-
|
| 494 |
-
SECTION A
|
| 495 |
-
|
| 496 |
Full Name : {name}
|
| 497 |
CNIC : {cnic}
|
| 498 |
Contact : {phone if phone else "Not Provided"}
|
| 499 |
City : {city}
|
| 500 |
Area : {location}
|
| 501 |
|
| 502 |
-
|
| 503 |
-
SECTION B
|
| 504 |
-
|
| 505 |
Nature : {issue_type}
|
| 506 |
Location : {location}, {city}
|
| 507 |
Date & Time : {date_str}, {time_str}
|
|
@@ -510,63 +514,57 @@ def build_professional_report(complaint_id, timestamp, name, cnic, phone, city,
|
|
| 510 |
Description:
|
| 511 |
{description.strip() if description.strip() else "[No additional description provided]"}
|
| 512 |
|
| 513 |
-
|
| 514 |
-
SECTION C
|
| 515 |
-
|
| 516 |
Status : {gemini_status}
|
| 517 |
Confidence : {gemini_confidence}
|
| 518 |
Finding : {gemini_reason}
|
| 519 |
Action : {gemini_action if gemini_action else "Immediate field inspection required."}
|
| 520 |
YOLO : {yolo_summary}
|
| 521 |
|
| 522 |
-
|
| 523 |
-
SECTION D
|
| 524 |
-
|
| 525 |
-
Applicable Legislation:
|
| 526 |
-
for law in kb.get("laws", []):
|
| 527 |
-
report += f"\n - {law}"
|
| 528 |
-
report += f"""
|
| 529 |
|
| 530 |
Responsible Authority : {kb.get('authority','N/A')}
|
| 531 |
Official Helpline : {kb.get('hotline','N/A')}
|
| 532 |
Statutory Response Time: {kb.get('response','N/A')}
|
| 533 |
Fine / Penalty : {kb.get('fine','N/A')}
|
| 534 |
|
| 535 |
-
|
| 536 |
-
SECTION E
|
| 537 |
-
|
| 538 |
-
for right in kb.get("citizen_rights", []):
|
| 539 |
-
report += f"\n - {right}"
|
| 540 |
-
report += f"""
|
| 541 |
|
| 542 |
Escalation : {kb.get('escalation','CM Portal: 0800-02345')}
|
| 543 |
|
| 544 |
-
|
| 545 |
-
SECTION F
|
| 546 |
-
|
| 547 |
{local_msg}
|
| 548 |
|
| 549 |
-
|
| 550 |
-
SECTION G
|
| 551 |
-
|
| 552 |
MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}
|
| 553 |
|
| 554 |
Authority: {kb.get('authority','Local Authority')}
|
| 555 |
Hotline : {kb.get('hotline','N/A')}
|
| 556 |
Portal : citizenportal.gov.pk | CM: 0800-02345
|
| 557 |
|
| 558 |
-
|
| 559 |
DECLARATION
|
| 560 |
-
|
| 561 |
I, {name} (CNIC: {cnic}), declare that the information provided is
|
| 562 |
true and correct to the best of my knowledge.
|
| 563 |
|
| 564 |
Signature: ________________________ (Digital) Date: {date_str}
|
| 565 |
Reference: {complaint_id}
|
| 566 |
|
| 567 |
-
|
| 568 |
FOR OFFICIAL USE ONLY
|
| 569 |
-
|
| 570 |
Received By : ___________________________
|
| 571 |
Date of Receipt : ___________________________
|
| 572 |
Action Taken : ___________________________
|
|
@@ -574,7 +572,6 @@ def build_professional_report(complaint_id, timestamp, name, cnic, phone, city,
|
|
| 574 |
|
| 575 |
Generated by: Rahbar - Pakistan's AI Civic Redressal Platform
|
| 576 |
Timestamp: {timestamp}
|
| 577 |
-
===========================================================================
|
| 578 |
"""
|
| 579 |
return report
|
| 580 |
|
|
@@ -618,7 +615,6 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
|
|
| 618 |
local = LOCALIZED.get(clean_issue, {}).get(language, "")
|
| 619 |
sev_icon = severity_icon(final_severity)
|
| 620 |
|
| 621 |
-
# Legal advice in user's selected language
|
| 622 |
llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
|
| 623 |
|
| 624 |
report = build_professional_report(
|
|
@@ -641,7 +637,6 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
|
|
| 641 |
f"Filed: {timestamp}")
|
| 642 |
wa_md = f"[Share on WhatsApp]({make_whatsapp_link(wa_text)})"
|
| 643 |
|
| 644 |
-
# Report TTS in selected language, includes localized law notice
|
| 645 |
report_tts_path = None
|
| 646 |
if enable_tts:
|
| 647 |
tts_text = (f"Complaint {complaint_id} filed. "
|
|
@@ -649,53 +644,267 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
|
|
| 649 |
f"Severity: {final_severity} out of 10. {local}")
|
| 650 |
report_tts_path = make_tts(tts_text, language)
|
| 651 |
|
| 652 |
-
# Legal advice TTS always generated in selected language
|
| 653 |
advice_tts_path = make_tts(llama_advice[:600], language)
|
| 654 |
|
| 655 |
return annotated_img, report, wa_md, llama_advice, report_tts_path, complaint_id, advice_tts_path
|
| 656 |
|
| 657 |
-
# ─── CSS ──────────────────────────
|
| 658 |
CSS = """
|
| 659 |
@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap');
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
--
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
"""
|
| 700 |
|
| 701 |
HEADER_HTML = """
|
|
@@ -714,33 +923,30 @@ HEADER_HTML = """
|
|
| 714 |
</div>
|
| 715 |
"""
|
| 716 |
|
|
|
|
| 717 |
MAP_HTML = """
|
| 718 |
<div style="margin:14px 0 4px">
|
| 719 |
<div class="card-title">GPS / Map Location Picker</div>
|
| 720 |
<div class="warn-box">
|
| 721 |
-
<strong>Please allow location access</strong> when your browser asks
|
| 722 |
-
Tap the button below
|
| 723 |
-
|
| 724 |
-
<br><strong>براہ کرم Location Allow کریں</strong> جب browser پوچھے۔
|
| 725 |
</div>
|
| 726 |
|
| 727 |
-
<button
|
| 728 |
-
|
| 729 |
-
border-radius:10px;padding:10px 20px;font-size:.9rem;font-weight:600;
|
| 730 |
-
cursor:pointer;width:100%;margin-bottom:8px;display:block"
|
| 731 |
-
onclick="window.rahbarGetGPS()">
|
| 732 |
-
Use My Current GPS Location / اپنی Location استعمال کریں
|
| 733 |
</button>
|
| 734 |
|
| 735 |
<div id="gpsStatus"
|
| 736 |
-
style="font-size:.
|
| 737 |
-
|
|
|
|
| 738 |
</div>
|
| 739 |
|
| 740 |
<div id="rahbarMapDiv"></div>
|
| 741 |
|
| 742 |
-
<p style="font-size:.72rem;color:#
|
| 743 |
-
Tap map to drop pin · Drag pin to adjust · GPS
|
| 744 |
</p>
|
| 745 |
</div>
|
| 746 |
|
|
@@ -753,40 +959,94 @@ MAP_HTML = """
|
|
| 753 |
|
| 754 |
function setStatus(msg, color){
|
| 755 |
var el = document.getElementById('gpsStatus');
|
| 756 |
-
if(el)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
}
|
| 758 |
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
|
|
|
| 762 |
document.querySelectorAll('textarea, input[type=text]').forEach(function(el){
|
| 763 |
-
|
|
|
|
| 764 |
var lbl = '';
|
| 765 |
var blk = el.closest('.block');
|
| 766 |
if(blk){ var l = blk.querySelector('label'); if(l) lbl = l.textContent.toLowerCase(); }
|
| 767 |
-
if(ph.indexOf('gps') > -1 || ph.indexOf('street') > -1
|
| 768 |
-
||
|
| 769 |
-
|
| 770 |
-
var
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
}
|
| 775 |
});
|
| 776 |
}
|
| 777 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
function initMap(lat, lng){
|
| 779 |
if(!leafletReady){ setTimeout(function(){ initMap(lat, lng); }, 400); return; }
|
| 780 |
var el = document.getElementById('rahbarMapDiv');
|
| 781 |
if(!el) return;
|
| 782 |
if(rbMap){ rbMap.remove(); rbMap = null; }
|
| 783 |
-
rbMap = L.map('rahbarMapDiv').setView([lat, lng],
|
| 784 |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
| 785 |
-
{attribution:'© OpenStreetMap', maxZoom:19}).addTo(rbMap);
|
| 786 |
-
rbMark = L.marker([lat, lng], {draggable:true}).addTo(rbMap)
|
| 787 |
-
|
| 788 |
-
rbMark.on('dragend', function(e){
|
| 789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
setTimeout(function(){ if(rbMap) rbMap.invalidateSize(); }, 350);
|
| 791 |
}
|
| 792 |
|
|
@@ -794,26 +1054,27 @@ MAP_HTML = """
|
|
| 794 |
if(!navigator.geolocation){
|
| 795 |
setStatus('GPS not supported by this browser.', '#e85353'); return;
|
| 796 |
}
|
| 797 |
-
setStatus('Requesting location
|
| 798 |
navigator.geolocation.getCurrentPosition(
|
| 799 |
function(pos){
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
|
|
|
| 803 |
},
|
| 804 |
function(err){
|
| 805 |
var msgs = {
|
| 806 |
-
1: 'DENIED
|
| 807 |
-
2: 'Location unavailable. Check
|
| 808 |
-
3: 'Timeout
|
| 809 |
};
|
| 810 |
setStatus(msgs[err.code] || 'GPS error: ' + err.message, '#e85353');
|
| 811 |
},
|
| 812 |
-
{enableHighAccuracy:true, timeout:15000, maximumAge:0}
|
| 813 |
);
|
| 814 |
};
|
| 815 |
|
| 816 |
-
/* Load Leaflet JS
|
| 817 |
function loadLeaflet(){
|
| 818 |
if(typeof L !== 'undefined'){ leafletReady = true; initMap(DEFAULT_LAT, DEFAULT_LNG); return; }
|
| 819 |
var s = document.createElement('script');
|
|
@@ -840,7 +1101,7 @@ def update_areas(city):
|
|
| 840 |
|
| 841 |
# ─── BUILD UI ─────────────────────────────────────────────────
|
| 842 |
def build_ui():
|
| 843 |
-
with gr.Blocks(title="Rahbar | رہبر") as demo:
|
| 844 |
gr.HTML(HEADER_HTML)
|
| 845 |
|
| 846 |
with gr.Tabs():
|
|
@@ -868,7 +1129,7 @@ def build_ui():
|
|
| 868 |
|
| 869 |
location_tb = gr.Textbox(
|
| 870 |
label="Street / Landmark / GPS Coordinates",
|
| 871 |
-
placeholder="GPS
|
| 872 |
lines=1
|
| 873 |
)
|
| 874 |
desc_tb = gr.Textbox(label="Description (optional)", placeholder="Describe the issue...", lines=3)
|
|
@@ -932,13 +1193,7 @@ def build_ui():
|
|
| 932 |
or your legal rights as a Pakistani citizen.
|
| 933 |
</div>""")
|
| 934 |
chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
|
| 935 |
-
|
| 936 |
-
# KEY FIX: type="messages" for Gradio 6+
|
| 937 |
-
chatbot = gr.Chatbot(
|
| 938 |
-
label="Rahbar Legal Assistant",
|
| 939 |
-
height=600,
|
| 940 |
-
value=[],
|
| 941 |
-
)
|
| 942 |
with gr.Row():
|
| 943 |
chat_input = gr.Textbox(
|
| 944 |
label="Your Question",
|
|
@@ -957,7 +1212,6 @@ def build_ui():
|
|
| 957 |
inputs=[chat_input, chatbot, chat_lang_dd],
|
| 958 |
outputs=[chatbot, chat_input]
|
| 959 |
)
|
| 960 |
-
|
| 961 |
gr.Examples(
|
| 962 |
examples=[
|
| 963 |
["WASA did not fix the leakage for 3 days. What are my rights?"],
|
|
@@ -975,14 +1229,9 @@ def build_ui():
|
|
| 975 |
gr.HTML('<div class="card-title">Speech-to-Text (STT)</div>')
|
| 976 |
gr.HTML('<div class="warn-box">Record your complaint. Copy the transcription to the Report Issue tab.</div>')
|
| 977 |
gr.HTML('<div class="info-box">Allow microphone access when your browser asks. Works best in Chrome/Edge.<br>مائیکروفون کی اجازت دیں جب browser پوچھے۔</div>')
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
label="Record or Upload Audio",
|
| 982 |
-
sources=["microphone", "upload"]
|
| 983 |
-
)
|
| 984 |
-
stt_btn = gr.Button("Transcribe Audio", variant="primary")
|
| 985 |
-
stt_out = gr.Textbox(label="Transcription (editable — copy to Report tab)", lines=5, interactive=True)
|
| 986 |
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
|
| 987 |
|
| 988 |
gr.HTML('<div class="card-title" style="margin-top:20px">Text-to-Speech Test</div>')
|
|
@@ -1010,8 +1259,6 @@ if __name__ == "__main__":
|
|
| 1010 |
demo.launch(
|
| 1011 |
server_name="0.0.0.0",
|
| 1012 |
server_port=7860,
|
| 1013 |
-
|
| 1014 |
-
css=CSS,
|
| 1015 |
share=True,
|
| 1016 |
theme=gr.themes.Base(
|
| 1017 |
primary_hue=gr.themes.colors.green,
|
|
|
|
| 3 |
║ Rahbar | رہبر ║
|
| 4 |
║ Pakistan's AI-Powered Civic Complaint System ║
|
| 5 |
╚══════════════════════════════════════════════════════════════╝
|
| 6 |
+
Changes:
|
| 7 |
+
• GPS location button now fills location field via reverse geocoding
|
| 8 |
+
• Light/dark mode adaptive color scheme
|
| 9 |
+
• Report output: bold headings, no === lines around them
|
| 10 |
• Chatbot uses {"role","content"} dict format (Gradio 6+)
|
|
|
|
| 11 |
• Report TTS in user-selected language
|
| 12 |
• Voice input (STT) working
|
| 13 |
• پنجابی language added
|
|
|
|
| 294 |
|
| 295 |
# ─── CHATBOT — Gradio 6+ dict format ─────────────────────────
|
| 296 |
def legal_chatbot(user_message, history, language):
|
|
|
|
|
|
|
|
|
|
| 297 |
if history is None:
|
| 298 |
history = []
|
| 299 |
if not user_message.strip():
|
|
|
|
| 347 |
try:
|
| 348 |
from groq import Groq
|
| 349 |
client = Groq(api_key=GROQ_API_KEY)
|
|
|
|
| 350 |
api_messages = [{"role": "system", "content": system_content}]
|
| 351 |
for msg in history[-20:]:
|
| 352 |
api_messages.append({"role": msg["role"], "content": msg["content"]})
|
|
|
|
| 470 |
if score <= 8: return "ORANGE"
|
| 471 |
return "RED"
|
| 472 |
|
| 473 |
+
# ─── PROFESSIONAL REPORT — clean bold headings, no === lines ──
|
| 474 |
def build_professional_report(complaint_id, timestamp, name, cnic, phone, city, location,
|
| 475 |
issue_type, language, severity, sev_icon,
|
| 476 |
yolo_summary, gemini_status, gemini_reason,
|
| 477 |
gemini_confidence, gemini_action, kb, description, local_msg):
|
| 478 |
date_str = datetime.datetime.now().strftime("%d %B %Y")
|
| 479 |
time_str = datetime.datetime.now().strftime("%I:%M %p")
|
| 480 |
+
|
| 481 |
+
laws_text = ""
|
| 482 |
+
for law in kb.get("laws", []):
|
| 483 |
+
laws_text += f"\n - {law}"
|
| 484 |
+
|
| 485 |
+
rights_text = ""
|
| 486 |
+
for right in kb.get("citizen_rights", []):
|
| 487 |
+
rights_text += f"\n - {right}"
|
| 488 |
+
|
| 489 |
+
report = f""" GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT
|
| 490 |
+
Rahbar Digital Civic Redressal System
|
| 491 |
|
| 492 |
Complaint Reference No. : {complaint_id}
|
| 493 |
Date of Filing : {date_str} Time: {time_str}
|
| 494 |
Filing Method : Rahbar Digital Platform (AI-Verified)
|
| 495 |
Report Language : {language}
|
| 496 |
|
| 497 |
+
|
| 498 |
+
SECTION A — COMPLAINANT INFORMATION
|
| 499 |
+
|
| 500 |
Full Name : {name}
|
| 501 |
CNIC : {cnic}
|
| 502 |
Contact : {phone if phone else "Not Provided"}
|
| 503 |
City : {city}
|
| 504 |
Area : {location}
|
| 505 |
|
| 506 |
+
|
| 507 |
+
SECTION B — COMPLAINT DETAILS
|
| 508 |
+
|
| 509 |
Nature : {issue_type}
|
| 510 |
Location : {location}, {city}
|
| 511 |
Date & Time : {date_str}, {time_str}
|
|
|
|
| 514 |
Description:
|
| 515 |
{description.strip() if description.strip() else "[No additional description provided]"}
|
| 516 |
|
| 517 |
+
|
| 518 |
+
SECTION C — AI VERIFICATION RESULTS
|
| 519 |
+
|
| 520 |
Status : {gemini_status}
|
| 521 |
Confidence : {gemini_confidence}
|
| 522 |
Finding : {gemini_reason}
|
| 523 |
Action : {gemini_action if gemini_action else "Immediate field inspection required."}
|
| 524 |
YOLO : {yolo_summary}
|
| 525 |
|
| 526 |
+
|
| 527 |
+
SECTION D — LEGAL FRAMEWORK
|
| 528 |
+
|
| 529 |
+
Applicable Legislation:{laws_text}
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
Responsible Authority : {kb.get('authority','N/A')}
|
| 532 |
Official Helpline : {kb.get('hotline','N/A')}
|
| 533 |
Statutory Response Time: {kb.get('response','N/A')}
|
| 534 |
Fine / Penalty : {kb.get('fine','N/A')}
|
| 535 |
|
| 536 |
+
|
| 537 |
+
SECTION E — CITIZEN'S LEGAL RIGHTS
|
| 538 |
+
{rights_text}
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
Escalation : {kb.get('escalation','CM Portal: 0800-02345')}
|
| 541 |
|
| 542 |
+
|
| 543 |
+
SECTION F — LOCALIZED NOTICE ({language})
|
| 544 |
+
|
| 545 |
{local_msg}
|
| 546 |
|
| 547 |
+
|
| 548 |
+
SECTION G — ACTION DIRECTIVE
|
| 549 |
+
|
| 550 |
MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}
|
| 551 |
|
| 552 |
Authority: {kb.get('authority','Local Authority')}
|
| 553 |
Hotline : {kb.get('hotline','N/A')}
|
| 554 |
Portal : citizenportal.gov.pk | CM: 0800-02345
|
| 555 |
|
| 556 |
+
|
| 557 |
DECLARATION
|
| 558 |
+
|
| 559 |
I, {name} (CNIC: {cnic}), declare that the information provided is
|
| 560 |
true and correct to the best of my knowledge.
|
| 561 |
|
| 562 |
Signature: ________________________ (Digital) Date: {date_str}
|
| 563 |
Reference: {complaint_id}
|
| 564 |
|
| 565 |
+
|
| 566 |
FOR OFFICIAL USE ONLY
|
| 567 |
+
|
| 568 |
Received By : ___________________________
|
| 569 |
Date of Receipt : ___________________________
|
| 570 |
Action Taken : ___________________________
|
|
|
|
| 572 |
|
| 573 |
Generated by: Rahbar - Pakistan's AI Civic Redressal Platform
|
| 574 |
Timestamp: {timestamp}
|
|
|
|
| 575 |
"""
|
| 576 |
return report
|
| 577 |
|
|
|
|
| 615 |
local = LOCALIZED.get(clean_issue, {}).get(language, "")
|
| 616 |
sev_icon = severity_icon(final_severity)
|
| 617 |
|
|
|
|
| 618 |
llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
|
| 619 |
|
| 620 |
report = build_professional_report(
|
|
|
|
| 637 |
f"Filed: {timestamp}")
|
| 638 |
wa_md = f"[Share on WhatsApp]({make_whatsapp_link(wa_text)})"
|
| 639 |
|
|
|
|
| 640 |
report_tts_path = None
|
| 641 |
if enable_tts:
|
| 642 |
tts_text = (f"Complaint {complaint_id} filed. "
|
|
|
|
| 644 |
f"Severity: {final_severity} out of 10. {local}")
|
| 645 |
report_tts_path = make_tts(tts_text, language)
|
| 646 |
|
|
|
|
| 647 |
advice_tts_path = make_tts(llama_advice[:600], language)
|
| 648 |
|
| 649 |
return annotated_img, report, wa_md, llama_advice, report_tts_path, complaint_id, advice_tts_path
|
| 650 |
|
| 651 |
+
# ─── CSS — Light/Dark mode adaptive ──────────────────────────
|
| 652 |
CSS = """
|
| 653 |
@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap');
|
| 654 |
+
|
| 655 |
+
/* ── Light mode palette ── */
|
| 656 |
+
:root {
|
| 657 |
+
--g9:#0d2b1e; --g8:#14432e; --g7:#1a5c3f; --g6:#1f7a52;
|
| 658 |
+
--g5:#25a06b; --g4:#2ec97f;
|
| 659 |
+
--a5:#f5a623; --a4:#d4870e; --a3:#b8730c;
|
| 660 |
+
--surface:#ffffff; --surface2:#f4f8f5; --surface3:#e8f3ed;
|
| 661 |
+
--text-primary:#0d2b1e; --text-secondary:#2d5a3e; --text-muted:#5a8a6e;
|
| 662 |
+
--border:#b8d9c5; --border-strong:#1f7a52;
|
| 663 |
+
--shadow:0 2px 8px rgba(13,43,30,.10);
|
| 664 |
+
--radius:12px; --radius-lg:20px;
|
| 665 |
+
--header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#0a1f14 100%);
|
| 666 |
+
--badge-bg:#ffffff; --badge-border:#1a5c3f;
|
| 667 |
+
--warn-bg:#fffbf0; --warn-border:rgba(245,166,35,.5);
|
| 668 |
+
--info-bg:#f0faf4; --info-border:#1f7a52;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
/* ── Dark mode palette ── */
|
| 672 |
+
@media (prefers-color-scheme: dark) {
|
| 673 |
+
:root {
|
| 674 |
+
--g9:#d0f0df; --g8:#1e6644; --g7:#28885a; --g6:#30aa72;
|
| 675 |
+
--g5:#3dcf8a; --g4:#5de3a3;
|
| 676 |
+
--a5:#f7bc57; --a4:#f5a623; --a3:#f0921a;
|
| 677 |
+
--surface:#0f1f16; --surface2:#162b1e; --surface3:#1c3828;
|
| 678 |
+
--text-primary:#d0f0df; --text-secondary:#8fcfae; --text-muted:#5a9a78;
|
| 679 |
+
--border:#2a5c3e; --border-strong:#30aa72;
|
| 680 |
+
--shadow:0 2px 12px rgba(0,0,0,.4);
|
| 681 |
+
--header-bg:linear-gradient(135deg,#0a1f14 0%,#071510 60%,#050f0a 100%);
|
| 682 |
+
--badge-bg:#0f1f16; --badge-border:#28885a;
|
| 683 |
+
--warn-bg:#1e1800; --warn-border:rgba(247,188,87,.4);
|
| 684 |
+
--info-bg:#0e1f16; --info-border:#30aa72;
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 689 |
+
|
| 690 |
+
body, .gradio-container {
|
| 691 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 692 |
+
background: var(--surface) !important;
|
| 693 |
+
color: var(--text-primary) !important;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
/* ── Header ── */
|
| 697 |
+
.saaf-header {
|
| 698 |
+
background: var(--header-bg);
|
| 699 |
+
border-bottom: 2px solid var(--g6);
|
| 700 |
+
padding: 24px 20px 20px;
|
| 701 |
+
text-align: center;
|
| 702 |
+
position: relative;
|
| 703 |
+
overflow: hidden;
|
| 704 |
+
}
|
| 705 |
+
.saaf-header::before {
|
| 706 |
+
content: '';
|
| 707 |
+
position: absolute; inset: 0;
|
| 708 |
+
background: radial-gradient(ellipse 60% 60% at 50% 0%, rgba(37,160,107,.18), transparent);
|
| 709 |
+
pointer-events: none;
|
| 710 |
+
}
|
| 711 |
+
.saaf-header h1 {
|
| 712 |
+
font-family: 'Playfair Display', serif !important;
|
| 713 |
+
font-size: clamp(1.8rem, 5vw, 3rem) !important;
|
| 714 |
+
font-weight: 900 !important;
|
| 715 |
+
color: #fafdf8 !important;
|
| 716 |
+
margin: 0 0 4px !important;
|
| 717 |
+
line-height: 1.1;
|
| 718 |
+
}
|
| 719 |
+
.saaf-header .urdu-title {
|
| 720 |
+
font-family: 'Noto Nastaliq Urdu', serif;
|
| 721 |
+
font-size: clamp(1rem, 3vw, 1.6rem);
|
| 722 |
+
color: #f7bc57;
|
| 723 |
+
direction: rtl;
|
| 724 |
+
margin: 4px 0 8px;
|
| 725 |
+
}
|
| 726 |
+
.saaf-header .tagline {
|
| 727 |
+
font-size: clamp(.75rem, 2vw, .95rem);
|
| 728 |
+
color: #5de3a3;
|
| 729 |
+
letter-spacing: .08em;
|
| 730 |
+
text-transform: uppercase;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
/* ── Badge strip ── */
|
| 734 |
+
.badge-strip {
|
| 735 |
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
|
| 736 |
+
padding: 10px 16px;
|
| 737 |
+
background: var(--surface2);
|
| 738 |
+
border-bottom: 1px solid var(--border);
|
| 739 |
+
}
|
| 740 |
+
.badge {
|
| 741 |
+
font-size: .72rem; font-weight: 600; letter-spacing: .06em;
|
| 742 |
+
padding: 4px 12px; border-radius: 20px; text-transform: uppercase;
|
| 743 |
+
}
|
| 744 |
+
.badge-ai { background: var(--badge-bg); color: var(--g4); border: 1px solid var(--badge-border); }
|
| 745 |
+
.badge-pk { background: var(--badge-bg); color: var(--a4); border: 1px solid var(--warn-border); }
|
| 746 |
+
.badge-live{ background: var(--badge-bg); color: #ff8080; border: 1px solid rgba(232,83,83,.4); }
|
| 747 |
+
|
| 748 |
+
/* ── Tabs ── */
|
| 749 |
+
.tab-nav { background: var(--surface2) !important; border-bottom: 2px solid var(--border) !important; }
|
| 750 |
+
.tab-nav button {
|
| 751 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 752 |
+
font-weight: 500 !important; font-size: .85rem !important;
|
| 753 |
+
color: var(--text-muted) !important;
|
| 754 |
+
padding: 12px 18px !important; border-radius: 0 !important;
|
| 755 |
+
transition: all .2s !important;
|
| 756 |
+
}
|
| 757 |
+
.tab-nav button.selected,
|
| 758 |
+
.tab-nav button[aria-selected="true"] {
|
| 759 |
+
color: var(--a4) !important;
|
| 760 |
+
border-bottom: 3px solid var(--a5) !important;
|
| 761 |
+
background: transparent !important;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
/* ── Card title ── */
|
| 765 |
+
.card-title {
|
| 766 |
+
font-size: .7rem; font-weight: 600; letter-spacing: .1em;
|
| 767 |
+
text-transform: uppercase; color: var(--g4);
|
| 768 |
+
margin-bottom: 12px; padding-bottom: 8px;
|
| 769 |
+
border-bottom: 1px solid var(--border);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ── Labels ── */
|
| 773 |
+
label, .gradio-container .label-wrap span {
|
| 774 |
+
color: var(--text-primary) !important;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
/* ── Inputs ── */
|
| 778 |
+
.gradio-container input,
|
| 779 |
+
.gradio-container textarea {
|
| 780 |
+
background: var(--surface) !important;
|
| 781 |
+
border: 1px solid var(--border-strong) !important;
|
| 782 |
+
border-radius: var(--radius) !important;
|
| 783 |
+
color: var(--text-primary) !important;
|
| 784 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 785 |
+
}
|
| 786 |
+
.gradio-container input:focus,
|
| 787 |
+
.gradio-container textarea:focus {
|
| 788 |
+
border-color: var(--a5) !important;
|
| 789 |
+
outline: none !important;
|
| 790 |
+
box-shadow: 0 0 0 3px rgba(245,166,35,.15) !important;
|
| 791 |
+
}
|
| 792 |
+
.gradio-container .wrap { background: var(--surface) !important; border-color: var(--border-strong) !important; }
|
| 793 |
+
.gradio-container .block { background: var(--surface) !important; }
|
| 794 |
+
|
| 795 |
+
/* ── Buttons ── */
|
| 796 |
+
.gradio-container button.primary {
|
| 797 |
+
background: linear-gradient(135deg, var(--g6), var(--g5)) !important;
|
| 798 |
+
color: #fafdf8 !important;
|
| 799 |
+
border: none !important; border-radius: var(--radius) !important;
|
| 800 |
+
font-weight: 600 !important; font-size: .9rem !important;
|
| 801 |
+
padding: 12px 24px !important; cursor: pointer !important;
|
| 802 |
+
box-shadow: var(--shadow) !important; transition: all .2s !important;
|
| 803 |
+
}
|
| 804 |
+
.gradio-container button.primary:hover {
|
| 805 |
+
background: linear-gradient(135deg, var(--g5), var(--g4)) !important;
|
| 806 |
+
transform: translateY(-1px) !important;
|
| 807 |
+
}
|
| 808 |
+
.gradio-container button.secondary {
|
| 809 |
+
background: var(--surface) !important;
|
| 810 |
+
border: 1px solid var(--border-strong) !important;
|
| 811 |
+
color: var(--g4) !important;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
/* ── Image upload ── */
|
| 815 |
+
.gradio-container [data-testid="image"] {
|
| 816 |
+
border: 2px dashed var(--border-strong) !important;
|
| 817 |
+
border-radius: var(--radius-lg) !important;
|
| 818 |
+
background: var(--surface2) !important;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
/* ── Markdown headings ── */
|
| 822 |
+
.gradio-container .prose h2,
|
| 823 |
+
.gradio-container .prose h3 { color: var(--a4) !important; }
|
| 824 |
+
|
| 825 |
+
/* ── Audio ── */
|
| 826 |
+
.gradio-container audio { width: 100% !important; border-radius: var(--radius) !important; }
|
| 827 |
+
|
| 828 |
+
/* ── Info / warn boxes ── */
|
| 829 |
+
.info-box {
|
| 830 |
+
background: var(--info-bg);
|
| 831 |
+
border: 1px solid var(--info-border);
|
| 832 |
+
border-left: 4px solid var(--g5);
|
| 833 |
+
border-radius: var(--radius);
|
| 834 |
+
padding: 12px 16px;
|
| 835 |
+
font-size: .88rem; line-height: 1.6;
|
| 836 |
+
margin-bottom: 8px;
|
| 837 |
+
color: var(--text-secondary);
|
| 838 |
+
}
|
| 839 |
+
.warn-box {
|
| 840 |
+
background: var(--warn-bg);
|
| 841 |
+
border: 1px solid var(--warn-border);
|
| 842 |
+
border-left: 4px solid var(--a5);
|
| 843 |
+
border-radius: var(--radius);
|
| 844 |
+
padding: 12px 16px;
|
| 845 |
+
font-size: .88rem;
|
| 846 |
+
margin-bottom: 8px;
|
| 847 |
+
color: var(--text-secondary);
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
/* ── Hotline pill ── */
|
| 851 |
+
.hotline-pill {
|
| 852 |
+
display: inline-block;
|
| 853 |
+
background: var(--surface2); color: var(--a4);
|
| 854 |
+
border: 1px solid var(--warn-border);
|
| 855 |
+
border-radius: 20px; padding: 2px 12px;
|
| 856 |
+
font-size: .8rem; font-weight: 600;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
/* ── Map div ── */
|
| 860 |
+
#rahbarMapDiv {
|
| 861 |
+
height: 220px; width: 100%;
|
| 862 |
+
border-radius: 12px;
|
| 863 |
+
border: 2px solid var(--border-strong);
|
| 864 |
+
margin-bottom: 8px;
|
| 865 |
+
background: var(--surface3);
|
| 866 |
+
position: relative;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
/* ── GPS button inside HTML block ── */
|
| 870 |
+
.gps-btn {
|
| 871 |
+
background: linear-gradient(135deg, var(--g6), var(--g5)) !important;
|
| 872 |
+
color: #fafdf8 !important;
|
| 873 |
+
border: none !important; border-radius: 10px !important;
|
| 874 |
+
padding: 10px 20px !important; font-size: .9rem !important;
|
| 875 |
+
font-weight: 600 !important; cursor: pointer !important;
|
| 876 |
+
width: 100% !important; margin-bottom: 8px !important;
|
| 877 |
+
display: block !important;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
/* ── Report textbox — bold section headings ── */
|
| 881 |
+
.gradio-container textarea {
|
| 882 |
+
font-family: 'DM Mono', 'Courier New', monospace !important;
|
| 883 |
+
font-size: .82rem !important;
|
| 884 |
+
line-height: 1.7 !important;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
/* ── Dropdown ── */
|
| 888 |
+
.gradio-container select,
|
| 889 |
+
.gradio-container [data-testid="dropdown"] {
|
| 890 |
+
background: var(--surface) !important;
|
| 891 |
+
color: var(--text-primary) !important;
|
| 892 |
+
border-color: var(--border-strong) !important;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
/* ── Chatbot ── */
|
| 896 |
+
.gradio-container .message.user { background: var(--surface3) !important; color: var(--text-primary) !important; }
|
| 897 |
+
.gradio-container .message.bot { background: var(--surface2) !important; color: var(--text-primary) !important; }
|
| 898 |
+
|
| 899 |
+
/* ── Scrollbar ── */
|
| 900 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 901 |
+
::-webkit-scrollbar-track { background: var(--surface2); }
|
| 902 |
+
::-webkit-scrollbar-thumb { background: var(--g6); border-radius: 3px; }
|
| 903 |
+
|
| 904 |
+
@media (max-width: 640px) {
|
| 905 |
+
.saaf-header { padding: 16px 12px; }
|
| 906 |
+
.tab-nav button { padding: 10px 12px !important; font-size: .78rem !important; }
|
| 907 |
+
}
|
| 908 |
"""
|
| 909 |
|
| 910 |
HEADER_HTML = """
|
|
|
|
| 923 |
</div>
|
| 924 |
"""
|
| 925 |
|
| 926 |
+
# ─── MAP HTML — GPS fills location + reverse geocodes address ─
|
| 927 |
MAP_HTML = """
|
| 928 |
<div style="margin:14px 0 4px">
|
| 929 |
<div class="card-title">GPS / Map Location Picker</div>
|
| 930 |
<div class="warn-box">
|
| 931 |
+
<strong>Please allow location access</strong> when your browser asks.
|
| 932 |
+
Tap the button below — your address will auto-fill the location field below.
|
| 933 |
+
<br><strong>براہ کرم Location Allow کریں۔</strong> آپ کا پتہ خودبخود بھر جائے گا۔
|
|
|
|
| 934 |
</div>
|
| 935 |
|
| 936 |
+
<button class="gps-btn" onclick="window.rahbarGetGPS()">
|
| 937 |
+
📍 Use My Current GPS Location / اپنی Location استعمال کریں
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
</button>
|
| 939 |
|
| 940 |
<div id="gpsStatus"
|
| 941 |
+
style="font-size:.82rem;min-height:20px;margin-bottom:6px;
|
| 942 |
+
font-weight:500;padding:4px 2px;color:var(--g4,#25a06b)">
|
| 943 |
+
Map ready. Tap GPS button to auto-fill your location or tap the map to drop a pin.
|
| 944 |
</div>
|
| 945 |
|
| 946 |
<div id="rahbarMapDiv"></div>
|
| 947 |
|
| 948 |
+
<p style="font-size:.72rem;color:var(--text-muted,#5a8a6e);margin:4px 0 10px">
|
| 949 |
+
Tap map to drop pin · Drag pin to adjust · GPS fills address automatically
|
| 950 |
</p>
|
| 951 |
</div>
|
| 952 |
|
|
|
|
| 959 |
|
| 960 |
function setStatus(msg, color){
|
| 961 |
var el = document.getElementById('gpsStatus');
|
| 962 |
+
if(!el) return;
|
| 963 |
+
el.textContent = msg;
|
| 964 |
+
el.style.color = color || 'var(--g4, #25a06b)';
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
/* ── Reverse geocode using Nominatim (free, no key needed) ── */
|
| 968 |
+
function reverseGeocode(lat, lng, callback){
|
| 969 |
+
var url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='
|
| 970 |
+
+ lat + '&lon=' + lng + '&zoom=17&addressdetails=1';
|
| 971 |
+
fetch(url, { headers: { 'Accept-Language': 'en' } })
|
| 972 |
+
.then(function(r){ return r.json(); })
|
| 973 |
+
.then(function(data){
|
| 974 |
+
var a = data.address || {};
|
| 975 |
+
var parts = [];
|
| 976 |
+
// Build a readable address from parts
|
| 977 |
+
if(a.road || a.pedestrian || a.footway)
|
| 978 |
+
parts.push(a.road || a.pedestrian || a.footway);
|
| 979 |
+
if(a.suburb || a.neighbourhood || a.quarter)
|
| 980 |
+
parts.push(a.suburb || a.neighbourhood || a.quarter);
|
| 981 |
+
if(a.city_district || a.county)
|
| 982 |
+
parts.push(a.city_district || a.county);
|
| 983 |
+
if(parts.length === 0 && data.display_name)
|
| 984 |
+
parts.push(data.display_name.split(',').slice(0,3).join(',').trim());
|
| 985 |
+
var address = parts.join(', ') || ('GPS: ' + lat.toFixed(5) + ', ' + lng.toFixed(5));
|
| 986 |
+
callback(address);
|
| 987 |
+
})
|
| 988 |
+
.catch(function(){
|
| 989 |
+
callback('GPS: ' + lat.toFixed(5) + ', ' + lng.toFixed(5));
|
| 990 |
+
});
|
| 991 |
}
|
| 992 |
|
| 993 |
+
/* ── Fill Gradio location textbox ── */
|
| 994 |
+
function fillLocationField(text){
|
| 995 |
+
// Target the textarea/input whose label contains Street/Landmark/GPS
|
| 996 |
+
var filled = false;
|
| 997 |
document.querySelectorAll('textarea, input[type=text]').forEach(function(el){
|
| 998 |
+
if(filled) return;
|
| 999 |
+
var ph = (el.placeholder || '').toLowerCase();
|
| 1000 |
var lbl = '';
|
| 1001 |
var blk = el.closest('.block');
|
| 1002 |
if(blk){ var l = blk.querySelector('label'); if(l) lbl = l.textContent.toLowerCase(); }
|
| 1003 |
+
if(ph.indexOf('gps') > -1 || ph.indexOf('street') > -1
|
| 1004 |
+
|| ph.indexOf('landmark') > -1 || lbl.indexOf('street') > -1
|
| 1005 |
+
|| lbl.indexOf('landmark') > -1 || lbl.indexOf('gps') > -1){
|
| 1006 |
+
var proto = el.tagName === 'TEXTAREA'
|
| 1007 |
+
? HTMLTextAreaElement.prototype
|
| 1008 |
+
: HTMLInputElement.prototype;
|
| 1009 |
+
var desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
| 1010 |
+
if(desc && desc.set) desc.set.call(el, text); else el.value = text;
|
| 1011 |
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1012 |
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
| 1013 |
+
filled = true;
|
| 1014 |
}
|
| 1015 |
});
|
| 1016 |
}
|
| 1017 |
|
| 1018 |
+
function placePin(lat, lng){
|
| 1019 |
+
if(!rbMap) return;
|
| 1020 |
+
rbMark.setLatLng([lat, lng]);
|
| 1021 |
+
rbMap.setView([lat, lng], 17);
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
function handleCoords(lat, lng, source){
|
| 1025 |
+
placePin(lat, lng);
|
| 1026 |
+
setStatus('Getting address…', 'var(--a4, #f5a623)');
|
| 1027 |
+
reverseGeocode(lat, lng, function(address){
|
| 1028 |
+
fillLocationField(address);
|
| 1029 |
+
setStatus('✅ Location filled: ' + address);
|
| 1030 |
+
});
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
function initMap(lat, lng){
|
| 1034 |
if(!leafletReady){ setTimeout(function(){ initMap(lat, lng); }, 400); return; }
|
| 1035 |
var el = document.getElementById('rahbarMapDiv');
|
| 1036 |
if(!el) return;
|
| 1037 |
if(rbMap){ rbMap.remove(); rbMap = null; }
|
| 1038 |
+
rbMap = L.map('rahbarMapDiv').setView([lat, lng], 13);
|
| 1039 |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
| 1040 |
+
{ attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(rbMap);
|
| 1041 |
+
rbMark = L.marker([lat, lng], { draggable: true }).addTo(rbMap)
|
| 1042 |
+
.bindPopup('Drag or tap to set your location').openPopup();
|
| 1043 |
+
rbMark.on('dragend', function(e){
|
| 1044 |
+
var ll = e.target.getLatLng();
|
| 1045 |
+
handleCoords(ll.lat, ll.lng, 'drag');
|
| 1046 |
+
});
|
| 1047 |
+
rbMap.on('click', function(e){
|
| 1048 |
+
handleCoords(e.latlng.lat, e.latlng.lng, 'click');
|
| 1049 |
+
});
|
| 1050 |
setTimeout(function(){ if(rbMap) rbMap.invalidateSize(); }, 350);
|
| 1051 |
}
|
| 1052 |
|
|
|
|
| 1054 |
if(!navigator.geolocation){
|
| 1055 |
setStatus('GPS not supported by this browser.', '#e85353'); return;
|
| 1056 |
}
|
| 1057 |
+
setStatus('Requesting location… please ALLOW when browser asks.', 'var(--a5, #f5a623)');
|
| 1058 |
navigator.geolocation.getCurrentPosition(
|
| 1059 |
function(pos){
|
| 1060 |
+
var lat = pos.coords.latitude, lng = pos.coords.longitude;
|
| 1061 |
+
setStatus('GPS found! Getting your address…', 'var(--g4, #2ec97f)');
|
| 1062 |
+
if(!rbMap){ initMap(lat, lng); }
|
| 1063 |
+
handleCoords(lat, lng, 'gps');
|
| 1064 |
},
|
| 1065 |
function(err){
|
| 1066 |
var msgs = {
|
| 1067 |
+
1: '❌ DENIED — Allow location in browser settings, then retry.',
|
| 1068 |
+
2: '⚠️ Location unavailable. Check GPS/network and retry.',
|
| 1069 |
+
3: '⏱️ Timeout — please try again.'
|
| 1070 |
};
|
| 1071 |
setStatus(msgs[err.code] || 'GPS error: ' + err.message, '#e85353');
|
| 1072 |
},
|
| 1073 |
+
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
|
| 1074 |
);
|
| 1075 |
};
|
| 1076 |
|
| 1077 |
+
/* Load Leaflet JS */
|
| 1078 |
function loadLeaflet(){
|
| 1079 |
if(typeof L !== 'undefined'){ leafletReady = true; initMap(DEFAULT_LAT, DEFAULT_LNG); return; }
|
| 1080 |
var s = document.createElement('script');
|
|
|
|
| 1101 |
|
| 1102 |
# ─── BUILD UI ─────────────────────────────────────────────────
|
| 1103 |
def build_ui():
|
| 1104 |
+
with gr.Blocks(title="Rahbar | رہبر", css=CSS) as demo:
|
| 1105 |
gr.HTML(HEADER_HTML)
|
| 1106 |
|
| 1107 |
with gr.Tabs():
|
|
|
|
| 1129 |
|
| 1130 |
location_tb = gr.Textbox(
|
| 1131 |
label="Street / Landmark / GPS Coordinates",
|
| 1132 |
+
placeholder="GPS address auto-fills here — or type street/landmark",
|
| 1133 |
lines=1
|
| 1134 |
)
|
| 1135 |
desc_tb = gr.Textbox(label="Description (optional)", placeholder="Describe the issue...", lines=3)
|
|
|
|
| 1193 |
or your legal rights as a Pakistani citizen.
|
| 1194 |
</div>""")
|
| 1195 |
chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
|
| 1196 |
+
chatbot = gr.Chatbot(label="Rahbar Legal Assistant", height=600, value=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
with gr.Row():
|
| 1198 |
chat_input = gr.Textbox(
|
| 1199 |
label="Your Question",
|
|
|
|
| 1212 |
inputs=[chat_input, chatbot, chat_lang_dd],
|
| 1213 |
outputs=[chatbot, chat_input]
|
| 1214 |
)
|
|
|
|
| 1215 |
gr.Examples(
|
| 1216 |
examples=[
|
| 1217 |
["WASA did not fix the leakage for 3 days. What are my rights?"],
|
|
|
|
| 1229 |
gr.HTML('<div class="card-title">Speech-to-Text (STT)</div>')
|
| 1230 |
gr.HTML('<div class="warn-box">Record your complaint. Copy the transcription to the Report Issue tab.</div>')
|
| 1231 |
gr.HTML('<div class="info-box">Allow microphone access when your browser asks. Works best in Chrome/Edge.<br>مائیکروفون کی اجازت دیں جب browser پوچھے۔</div>')
|
| 1232 |
+
audio_in = gr.Audio(type="filepath", label="Record or Upload Audio", sources=["microphone", "upload"])
|
| 1233 |
+
stt_btn = gr.Button("Transcribe Audio", variant="primary")
|
| 1234 |
+
stt_out = gr.Textbox(label="Transcription (editable — copy to Report tab)", lines=5, interactive=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1235 |
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
|
| 1236 |
|
| 1237 |
gr.HTML('<div class="card-title" style="margin-top:20px">Text-to-Speech Test</div>')
|
|
|
|
| 1259 |
demo.launch(
|
| 1260 |
server_name="0.0.0.0",
|
| 1261 |
server_port=7860,
|
|
|
|
|
|
|
| 1262 |
share=True,
|
| 1263 |
theme=gr.themes.Base(
|
| 1264 |
primary_hue=gr.themes.colors.green,
|