Spaces:
Sleeping
Sleeping
| import subprocess, sys | |
| def _ensure(pkg, import_name=None): | |
| name = import_name or pkg | |
| try: | |
| __import__(name) | |
| except ImportError: | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", pkg, | |
| "--quiet", "--break-system-packages"], | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| _ensure("pymupdf", "fitz") | |
| _ensure("pypdf") | |
| import gradio as gr | |
| import os, base64, json, urllib.request, urllib.error, re | |
| CLAIMS_TYPES = [ | |
| "Motor β Third Party (Bumper to Bumper)", | |
| "Motor β Own Damage (Comprehensive)", | |
| ] | |
| DEMO_CLAIMS = { | |
| "Motor β Third Party (Bumper to Bumper)": { | |
| "claimant_name": "John Azzopardi", | |
| "policy_number": "MTR-2024-88421", | |
| "claim_reference": "CLM-2024-001847", | |
| "claim_date": "12 March 2025", | |
| "incident_date": "10 March 2025", | |
| "claim_type": "Third Party Motor β Rear End Collision", | |
| "incident_location": "Msida Valley Road, Malta", | |
| "incident_description": "Claimant's vehicle was stationary at traffic lights when struck from behind by third party vehicle (Toyota Corolla, reg ABC-123). Rear bumper and boot lid damaged. No bodily injury reported. Police report filed. Third party admitted fault at scene.", | |
| "claimant_contact": "+356 9912 3456", | |
| "insurer_name": "", | |
| "supporting_documents": ["Police report", "Repair estimate", "Photos of damage", "Witness statement"], | |
| "key_flags": ["Third party admitted liability", "No bodily injury", "Low complexity"] | |
| }, | |
| "Motor β Own Damage (Comprehensive)": { | |
| "claimant_name": "Maria Camilleri", | |
| "policy_number": "MTR-2024-77310", | |
| "claim_reference": "CLM-2024-002103", | |
| "claim_date": "5 April 2025", | |
| "incident_date": "3 April 2025", | |
| "claim_type": "Own Damage β Comprehensive", | |
| "incident_location": "St Julian's Bypass, Malta", | |
| "incident_description": "Claimant lost control on wet road and collided with central barrier. Front end damage including bonnet, radiator, and front bumper. Airbags deployed. Vehicle towed. Claimant uninjured. No third party involved.", | |
| "claimant_contact": "+356 7734 8821", | |
| "insurer_name": "", | |
| "supporting_documents": ["Police accident report", "Two repair estimates", "Vehicle photos", "Towing invoice"], | |
| "key_flags": ["Single vehicle accident", "High repair cost", "Airbag deployment β severity check needed"] | |
| }, | |
| } | |
| STAGES = [ | |
| {"name": "FNOL / Claim Intake", "key": "fnol"}, | |
| {"name": "Validation & Triage", "key": "valid"}, | |
| {"name": "Investigation & Evidence", "key": "invest"}, | |
| {"name": "Coverage & Adjudication", "key": "adjud"}, | |
| {"name": "Damage Assessment & Valuation", "key": "val"}, | |
| {"name": "Settlement & Payment Routing", "key": "settle"}, | |
| {"name": "Closure & Reporting", "key": "close"}, | |
| ] | |
| POLICY_RULES = { | |
| "Motor β Third Party (Bumper to Bumper)": [ | |
| "Third party liability must be established before payment", | |
| "Claim must be reported within 24 hours of incident", | |
| "Police report mandatory for all motor claims", | |
| "Repair estimates from approved repairers only", | |
| "No bodily injury component β separate policy required", | |
| "Vehicle must have valid roadworthiness certificate", | |
| ], | |
| "Motor β Own Damage (Comprehensive)": [ | |
| "Vehicle must be roadworthy and have valid certificate at time of loss", | |
| "Driver must hold valid licence for vehicle class", | |
| "Own-damage excess applies β standard β¬500", | |
| "Repairs must be authorised before commencement", | |
| "Independent surveyor report required for claims exceeding β¬3,000 β initial reserve allocated pending survey outcome", | |
| "Airbag deployment triggers mandatory engineering inspection", | |
| ], | |
| } | |
| RESERVE_BENCHMARKS = { | |
| "Motor β Third Party (Bumper to Bumper)": {"low": 1500, "mid": 3000, "high": 6000, "avg_duration": "3β6 weeks"}, | |
| "Motor β Own Damage (Comprehensive)": {"low": 2000, "mid": 5500, "high": 12000, "avg_duration": "4β8 weeks"}, | |
| } | |
| CSS = """ | |
| .gradio-container { max-width: 1200px !important; margin: 0 auto !important; font-family: 'Segoe UI', Arial, sans-serif !important; } | |
| footer { display: none !important; } | |
| .info-bar { background:#FFF4EF; border:1.5px solid #FC5108; border-radius:8px; padding:12px 16px; font-size:13px; color:#555; margin:16px 0 20px; line-height:1.5; } | |
| .step-title { font-size:18px; font-weight:700; color:#0E2841; margin-bottom:3px; } | |
| .step-sub { font-size:13px; color:#777; margin-bottom:16px; } | |
| .result-panel { background:#f8f9fa; border:1px solid #e4e4e4; border-radius:10px; padding:18px 20px; margin:10px 0; } | |
| .result-panel-title { font-size:14px; font-weight:700; color:#0E2841; margin-bottom:14px; padding-bottom:10px; border-bottom:1px solid #e4e4e4; } | |
| .field-grid { display:grid; grid-template-columns:160px 1fr; gap:0; } | |
| .field-key { color:#888; font-size:12px; font-weight:600; padding:6px 0; border-bottom:1px solid #f0f0f0; text-transform:uppercase; letter-spacing:.3px; } | |
| .field-val { color:#111; font-size:13px; padding:6px 0 6px 12px; border-bottom:1px solid #f0f0f0; } | |
| .policy-pass { background:#d4edda; border:1.5px solid #28a745; border-radius:8px; padding:10px 14px; margin:6px 0; } | |
| .policy-fail { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:10px 14px; margin:6px 0; } | |
| .policy-warn { background:#fff3cd; border:1.5px solid #ffc107; border-radius:8px; padding:10px 14px; margin:6px 0; } | |
| .policy-unknown { background:#e8eaf6; border:1.5px solid #7986cb; border-radius:8px; padding:10px 14px; margin:6px 0; } | |
| .stat-row { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin:14px 0; } | |
| .stat-box { background:#fff; border:1px solid #eee; border-radius:10px; padding:14px; text-align:center; } | |
| .stat-num { font-size:26px; font-weight:700; } | |
| .stat-lbl { font-size:11px; color:#888; margin-top:3px; text-transform:uppercase; letter-spacing:.4px; } | |
| .fraud-low { background:#d4edda; border:1.5px solid #28a745; border-radius:10px; padding:18px 20px; } | |
| .fraud-medium { background:#fff3cd; border:1.5px solid #ffc107; border-radius:10px; padding:18px 20px; } | |
| .fraud-high { background:#f8d7da; border:1.5px solid #dc3545; border-radius:10px; padding:18px 20px; } | |
| .action-chip { display:inline-block; padding:6px 18px; border-radius:7px; font-size:14px; font-weight:700; margin-bottom:10px; } | |
| .chip-approve { background:#28a745; color:white; } | |
| .chip-review { background:#ffc107; color:#333; } | |
| .chip-investigate { background:#FC5108; color:white; } | |
| .chip-decline { background:#dc3545; color:white; } | |
| .reserve-box { background:#fff; border:1.5px solid #ddd; border-radius:10px; padding:18px 20px; margin:10px 0; } | |
| .reserve-grid { display:grid; grid-template-columns:1fr; gap:12px; margin:14px 0; } | |
| .reserve-col { text-align:center; padding:14px 10px; border-radius:8px; } | |
| .reserve-mid { background:#fff3cd; border:1px solid #ffc107; } | |
| .reserve-amount { font-size:22px; font-weight:700; margin-bottom:4px; } | |
| .reserve-label { font-size:11px; text-transform:uppercase; letter-spacing:.4px; font-weight:600; } | |
| .reserve-mid .reserve-amount { color:#856404; } .reserve-mid .reserve-label { color:#856404; } | |
| .routing-approve { background:#d4edda; border:2px solid #28a745; border-radius:12px; padding:20px 22px; margin:10px 0; } | |
| .routing-review { background:#fff3cd; border:2px solid #ffc107; border-radius:12px; padding:20px 22px; margin:10px 0; } | |
| .routing-investigate{ background:#fff4ee; border:2px solid #FC5108; border-radius:12px; padding:20px 22px; margin:10px 0; } | |
| .routing-decline { background:#f8d7da; border:2px solid #dc3545; border-radius:12px; padding:20px 22px; margin:10px 0; } | |
| .coverage-banner-yes { background:#d4edda; border:1.5px solid #28a745; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#155724; } | |
| .coverage-banner-no { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#721c24; } | |
| .coverage-banner-unk { background:#fff3cd; border:1.5px solid #ffc107; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#856404; } | |
| .escalation-banner { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#721c24; font-weight:600; } | |
| .fault-banner { background:#e8f4fd; border:1.5px solid #4a90d9; border-radius:8px; padding:14px 16px; margin:10px 0; } | |
| """ | |
| # βββ AI HELPERS βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def call_claude(prompt, system="", image_b64=None, image_type="image/jpeg"): | |
| api_key = os.getenv("ANTHROPIC_API_KEY") | |
| if not api_key: return None, "No ANTHROPIC_API_KEY in Secrets" | |
| try: | |
| content = ([{"type":"image","source":{"type":"base64","media_type":image_type,"data":image_b64}}, | |
| {"type":"text","text":prompt}] if image_b64 else prompt) | |
| payload = json.dumps({ | |
| "model": "claude-opus-4-5", "max_tokens": 2500, | |
| "system": system or "You are a senior insurance AI consultant.", | |
| "messages": [{"role":"user","content":content}] | |
| }).encode() | |
| req = urllib.request.Request( | |
| "https://api.anthropic.com/v1/messages", data=payload, | |
| headers={"x-api-key":api_key,"anthropic-version":"2023-06-01","content-type":"application/json"}, | |
| method="POST") | |
| with urllib.request.urlopen(req, timeout=90) as r: | |
| return json.loads(r.read())["content"][0]["text"].strip(), None | |
| except urllib.error.HTTPError as e: | |
| return None, f"HTTP {e.code}: {e.read().decode()[:300]}" | |
| except Exception as e: | |
| return None, str(e) | |
| def call_gemini(prompt, system=""): | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| if not api_key: return None, "No GEMINI_API_KEY" | |
| try: | |
| payload = json.dumps({ | |
| "contents":[{"parts":[{"text": f"{system}\n\n{prompt}" if system else prompt}]}], | |
| "generationConfig":{"maxOutputTokens":2500} | |
| }).encode() | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}" | |
| req = urllib.request.Request(url, data=payload, headers={"content-type":"application/json"}, method="POST") | |
| with urllib.request.urlopen(req, timeout=90) as r: | |
| return json.loads(r.read())["candidates"][0]["content"]["parts"][0]["text"].strip(), None | |
| except Exception as e: return None, str(e) | |
| def ai_json(prompt, system="", image_b64=None, image_type="image/jpeg"): | |
| r, err = call_claude(prompt, system, image_b64, image_type) | |
| if not r and not image_b64: | |
| r, err = call_gemini(prompt, system) | |
| if not r: return None, err or "AI unavailable" | |
| clean = re.sub(r"```json|```","",r).strip() | |
| try: return json.loads(clean), None | |
| except: return None, f"JSON parse error: {clean[:200]}" | |
| def ai_text(prompt, system=""): | |
| r, _ = call_claude(prompt, system) | |
| if r: return r | |
| r2, _ = call_gemini(prompt, system) | |
| return r2 or "" | |
| def extract_file(filepath): | |
| if not filepath: return "", "No file" | |
| ext = os.path.splitext(filepath)[1].lower() | |
| if ext in [".txt",".csv"]: | |
| try: return open(filepath, errors="ignore").read()[:5000], "" | |
| except Exception as e: return "", str(e) | |
| if ext in [".jpg",".jpeg",".png",".webp",".bmp"]: | |
| try: | |
| raw = open(filepath,"rb").read() | |
| if len(raw)>4_000_000: return "", "Image >4MB" | |
| b64 = base64.b64encode(raw).decode() | |
| mime = {"jpg":"image/jpeg","jpeg":"image/jpeg","png":"image/png","webp":"image/webp","bmp":"image/bmp"}.get(ext.lstrip("."),"image/jpeg") | |
| r, err = call_claude("Extract ALL visible text from this insurance document. Return every field and value as 'Field: Value' pairs.", | |
| system="You are an expert OCR for insurance docs. Extract everything.", image_b64=b64, image_type=mime) | |
| return (r or ""), (err if not r else "") | |
| except Exception as e: return "", str(e) | |
| if ext == ".pdf": | |
| text = "" | |
| try: | |
| import fitz | |
| doc = fitz.open(filepath) | |
| for page in doc: text += page.get_text() | |
| doc.close() | |
| if text.strip(): return text[:5000].strip(), "" | |
| except: pass | |
| try: | |
| from pypdf import PdfReader | |
| for page in PdfReader(filepath).pages: text += (page.extract_text() or "") | |
| if text.strip(): return text[:5000].strip(), "" | |
| except: pass | |
| try: | |
| import fitz | |
| pdoc = fitz.open(filepath) | |
| pix = pdoc[0].get_pixmap(dpi=150) | |
| img_bytes = pix.tobytes("png") | |
| pdoc.close() | |
| b64v = base64.b64encode(img_bytes).decode() | |
| result, _ = call_claude( | |
| "Extract ALL visible text from this insurance document. Return every field and value as Field: Value pairs.", | |
| system="You are an expert OCR for insurance documents.", | |
| image_b64=b64v, image_type="image/png") | |
| if result: return result, "" | |
| except: pass | |
| return "", "Could not read PDF." | |
| return "", f"Unsupported file type: {ext}" | |
| def analyse_damage_photo(filepath): | |
| if not filepath: return {}, "No file" | |
| ext = os.path.splitext(filepath)[1].lower().lstrip(".") | |
| if ext not in ["jpg","jpeg","png","webp","bmp"]: | |
| return {}, f"Unsupported image type: {ext}" | |
| try: | |
| raw = open(filepath,"rb").read() | |
| if len(raw) > 4_000_000: return {}, "Image >4MB" | |
| b64 = base64.b64encode(raw).decode() | |
| mime = {"jpg":"image/jpeg","jpeg":"image/jpeg","png":"image/png","webp":"image/webp","bmp":"image/bmp"}.get(ext,"image/jpeg") | |
| prompt = ('This is a photo submitted as part of an insurance claim. Return ONLY JSON:\n' | |
| '{"damage_description":"1-2 sentences describing damage and severity",' | |
| '"number_plate_visible":true,"number_plate_text":"plate text or null"}') | |
| r, err = call_claude(prompt, system="You are an expert motor damage assessor. Return only valid JSON.", image_b64=b64, image_type=mime) | |
| if not r: return {}, err or "AI unavailable" | |
| clean = re.sub(r"```json|```","",r).strip() | |
| try: return json.loads(clean), "" | |
| except: return {"damage_description": r, "number_plate_visible": False, "number_plate_text": None}, "" | |
| except Exception as e: | |
| return {}, str(e) | |
| def analyse_document(filepath, claim_type, org, bumper_form_path=None, damage_photo_paths=None): | |
| text, err = extract_file(filepath) | |
| if err and not text and not bumper_form_path and not (damage_photo_paths or []): | |
| return None, "", f"β {err}" | |
| extra_sections = [] | |
| photo_findings = [] | |
| if bumper_form_path: | |
| b2b_text, _ = extract_file(bumper_form_path) | |
| if b2b_text: | |
| extra_sections.append(f"--- Bumper to Bumper Form ---\n{b2b_text[:2500]}") | |
| for p in (damage_photo_paths or []): | |
| if not p: continue | |
| finding, _ = analyse_damage_photo(p) | |
| if finding: | |
| photo_findings.append(finding) | |
| desc = finding.get("damage_description","") | |
| plate = finding.get("number_plate_text") | |
| section = f"--- Damage Photo ---\n{desc}" | |
| if plate: | |
| section += f"\nNumber plate: {plate}" | |
| extra_sections.append(section) | |
| combined_text = (text or "").strip() | |
| if extra_sections: | |
| combined_text = (combined_text + "\n\n" + "\n\n".join(extra_sections)).strip() | |
| if not combined_text: | |
| return None, "", "β No text extracted." | |
| prompt = f"""Insurance claim document for {org or 'insurer'}, type: {claim_type}. | |
| Raw text: {combined_text[:4500]} | |
| Return ONLY JSON (no markdown): | |
| {{"claimant_name":"...","policy_number":"...","claim_reference":"...","claim_date":"...","incident_date":"...","claim_type":"...","incident_location":"...","incident_description":"...","supporting_documents":["..."],"claimant_contact":"...","insurer_name":"...","vehicle_registration":"...","third_party_vehicle_registration":"...","key_flags":["..."]}}""" | |
| data, e2 = ai_json(prompt, system="Return only valid JSON.") | |
| data = data or {} | |
| if photo_findings: | |
| data["photo_findings"] = photo_findings | |
| return data, combined_text, e2 or "" | |
| def run_fnol_policy(claim_data, raw_text, claim_type, org, policy_file=None): | |
| policy_text = "" | |
| if policy_file: | |
| policy_text, _ = extract_file(policy_file) | |
| if not policy_text: | |
| rules = POLICY_RULES.get(claim_type, []) | |
| policy_text = f"Standard {claim_type} policy conditions:\n" + "\n".join(f"- {r}" for r in rules) | |
| claim_ctx = json.dumps(claim_data) if claim_data else raw_text[:2000] | |
| prompt = f"""Senior claims assessor reviewing FNOL for a {claim_type} claim at {org or 'insurer'}. | |
| CLAIM: {claim_ctx} | |
| POLICY: {policy_text[:3000]} | |
| Also assess fault probability: what is the probability (0-100%) that the insured is NOT at fault? | |
| Return ONLY JSON: | |
| {{"fnol_summary":"2-3 sentence summary","claim_validity":"VALID|POTENTIALLY VALID|REQUIRES INVESTIGATION|POTENTIALLY INVALID","validity_reason":"one sentence","policy_checks":[{{"rule":"...","status":"PASS|FAIL|WARNING|UNKNOWN","finding":"..."}}],"coverage_assessment":{{"likely_covered":true,"coverage_confidence":"HIGH|MEDIUM|LOW","coverage_notes":"..."}},"vehicle_match":{{"status":"MATCH|MISMATCH|NOT_DETECTED|NO_PHOTO_PROVIDED|NOT_APPLICABLE","claim_vehicle_registration":"...","photo_plate_numbers":["..."],"notes":"..."}},"fault_assessment":{{"probability_not_at_fault":85,"rationale":"one sentence explaining the fault assessment"}},"estimated_claim_validity_score":75,"complexity":"LOW|MEDIUM|HIGH","complexity_reason":"...","key_questions":["..."],"immediate_actions":["..."],"escalation_needed":false,"escalation_reason":null}}""" | |
| data, err = ai_json(prompt, system="You are an expert claims assessor. Return only valid JSON.") | |
| if data: return data, "" | |
| rules = POLICY_RULES.get(claim_type, []) | |
| return {"fnol_summary":"Manual review required.","claim_validity":"REQUIRES INVESTIGATION","validity_reason":"Automated analysis unavailable.","policy_checks":[{"rule":r,"status":"UNKNOWN","finding":"Manual check required"} for r in rules],"coverage_assessment":{"likely_covered":None,"coverage_confidence":"LOW","coverage_notes":"Manual assessment required"},"vehicle_match":{"status":"NOT_DETECTED","claim_vehicle_registration":(claim_data or {}).get("vehicle_registration"),"photo_plate_numbers":[],"notes":"Automated analysis unavailable."},"fault_assessment":{"probability_not_at_fault":50,"rationale":"Insufficient data for automated fault assessment."},"estimated_claim_validity_score":50,"complexity":"MEDIUM","complexity_reason":"Cannot determine.","key_questions":["Verify all claim details manually"],"immediate_actions":["Review claim file manually"],"escalation_needed":False,"escalation_reason":None}, "" | |
| def run_reserve(claim_data, fnol_data, claim_type, org): | |
| bench = RESERVE_BENCHMARKS.get(claim_type, {"low":1000,"mid":5000,"high":15000,"avg_duration":"unknown"}) | |
| prompt = f"""PwC actuarial consultant setting FNOL reserves for a {claim_type} claim. | |
| Benchmarks: Expected β¬{bench['mid']:,} | Duration: {bench['avg_duration']} | |
| Note: The repair amount is determined by a surveyor β not self-reported. An initial reserve estimate is uploaded per claim type and reviewed by the surveyor. | |
| CLAIM: {json.dumps(claim_data)[:2000]} | |
| FNOL: {json.dumps({k:fnol_data.get(k) for k in ["claim_validity","complexity","estimated_claim_validity_score"]})} | |
| Reserve increase triggers to consider (use exactly these): | |
| - Legal proceedings initiated | |
| - Medical complications arise | |
| - Third party injury claims submitted | |
| - Evidence of underinsurance identified | |
| - Liability remains disputed after investigation | |
| - Repair costs exceed initial surveyor estimate | |
| Next reserve review: minimum 30 days from today. | |
| Return ONLY JSON: | |
| {{"reserve_expected":0,"confidence":"HIGH|MEDIUM|LOW","currency":"EUR","rationale":"2-3 sentences β note that amount is subject to surveyor confirmation","key_drivers":["..."],"adjustment_triggers":["Legal proceedings initiated","Medical complications arise","Third party injury claims submitted","Evidence of underinsurance identified","Liability remains disputed after investigation","Repair costs exceed initial surveyor estimate"],"recommended_review":"Minimum 30 days from today","ibnr_note":"..."}}""" | |
| data, err = ai_json(prompt, system="You are an actuarial expert. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"reserve_expected":bench["mid"],"confidence":"MEDIUM","currency":"EUR","rationale":f"Initial benchmark reserve for {claim_type}. Subject to surveyor confirmation.","key_drivers":["Claim type benchmark"],"adjustment_triggers":["Legal proceedings initiated","Medical complications arise","Third party injury claims submitted","Evidence of underinsurance identified","Liability remains disputed after investigation","Repair costs exceed initial surveyor estimate"],"recommended_review":"Minimum 30 days from today","ibnr_note":"Standard IBNR provisions apply."}, "" | |
| def run_fraud(claim_data, fnol_data, claim_type): | |
| prompt = f"""Senior fraud investigator analysing a {claim_type} claim. | |
| Context: {json.dumps({"claim":claim_data,"fnol":fnol_data})[:3500]} | |
| Return ONLY JSON: | |
| {{"fraud_risk_level":"LOW|MEDIUM|HIGH","risk_score":45,"risk_factors":["..."],"positive_indicators":["..."],"red_flags":["..."],"recommended_action":"APPROVE|REVIEW|INVESTIGATE|DECLINE","action_reason":"one sentence","data_gaps":["..."],"fnol_consistency":"CONSISTENT|INCONSISTENT|PARTIALLY CONSISTENT","fnol_notes":"..."}}""" | |
| data, err = ai_json(prompt, system="You are an expert fraud detection AI. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"fraud_risk_level":"MEDIUM","risk_score":45,"risk_factors":["Insufficient data"],"positive_indicators":["Document provided"],"red_flags":[],"recommended_action":"REVIEW","action_reason":"Manual review recommended.","data_gaps":["API key required"],"fnol_consistency":"PARTIALLY CONSISTENT","fnol_notes":"Unable to cross-reference."}, "" | |
| def run_routing(claim_data, fnol_data, fraud_data, reserve_data, claim_type, org): | |
| prompt = f"""Claims adjudicator routing decision for a {claim_type} claim at {org or 'insurer'}. | |
| CLAIM: {json.dumps(claim_data)[:1000]} | |
| FNOL: validity={fnol_data.get('claim_validity')}, score={fnol_data.get('estimated_claim_validity_score')}, complexity={fnol_data.get('complexity')} | |
| FRAUD: risk={fraud_data.get('fraud_risk_level')}, score={fraud_data.get('risk_score')}, action={fraud_data.get('recommended_action')} | |
| RESERVE: expected=β¬{reserve_data.get('reserve_expected',0):,}, confidence={reserve_data.get('confidence')} | |
| Return ONLY JSON: | |
| {{"routing_decision":"STRAIGHT_TO_PAYMENT|FAST_TRACK_REVIEW|STANDARD_REVIEW|FULL_INVESTIGATION|DECLINE","confidence_score":85,"decision_rationale":"2-3 sentences","decision_factors":[{{"factor":"...","impact":"POSITIVE|NEGATIVE|NEUTRAL","weight":"HIGH|MEDIUM|LOW"}}],"estimated_settlement_days":5,"settlement_amount_recommendation":"...","conditions":["..."],"next_handler":"Automated Payment System|Junior Adjuster|Senior Adjuster|Special Investigations Unit|Legal Team","audit_trail":"one sentence"}}""" | |
| data, err = ai_json(prompt, system="You are an expert claims adjudicator. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"routing_decision":"STANDARD_REVIEW","confidence_score":60,"decision_rationale":"Manual review required.","decision_factors":[{"factor":"Manual review required","impact":"NEUTRAL","weight":"HIGH"}],"estimated_settlement_days":14,"settlement_amount_recommendation":"Per adjuster","conditions":["Manual review required"],"next_handler":"Senior Adjuster","audit_trail":"Claim routed for manual review."}, "" | |
| def run_survey(claim_data, fnol_data, claim_type, org, survey_file=None): | |
| survey_text = "" | |
| if survey_file: | |
| survey_text, _ = extract_file(survey_file) | |
| prompt = f"""Surveyor assessment for a {claim_type} claim at {org or 'insurer'}. | |
| {'Surveyor report content: ' + survey_text[:2000] if survey_text else 'Generate a sample surveyor assessment.'} | |
| CLAIM: {json.dumps(claim_data)[:1000]} | |
| Note: The repair/settlement amount is determined solely by the surveyor β not self-reported. | |
| Return ONLY JSON: | |
| {{"inspection_findings":"2-3 sentences","damage_description":"...","surveyor_repair_recommendation":"...","surveyor_cost_estimate":0,"currency":"EUR","discrepancies":["..."],"surveyor_overall_assessment":"...","approved_repairer":true,"additional_inspections_required":false}}""" | |
| data, err = ai_json(prompt, system="You are an expert motor surveyor. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"inspection_findings":"Manual surveyor inspection required.","damage_description":"To be confirmed by surveyor.","surveyor_repair_recommendation":"Pending inspection.","surveyor_cost_estimate":0,"currency":"EUR","discrepancies":[],"surveyor_overall_assessment":"Awaiting surveyor report.","approved_repairer":None,"additional_inspections_required":None}, "" | |
| def run_final_reserve(claim_data, fnol_data, reserve_data, survey_data, claim_type, org): | |
| surveyor_estimate = survey_data.get("surveyor_cost_estimate", 0) | |
| prompt = f"""Final reserve recommendation for a {claim_type} claim at {org or 'insurer'}. | |
| CLAIM: {json.dumps(claim_data)[:800]} | |
| INITIAL RESERVE: β¬{reserve_data.get('reserve_expected',0):,} | |
| SURVEYOR ESTIMATE: β¬{surveyor_estimate:,} | |
| FNOL validity: {fnol_data.get('claim_validity')} | Complexity: {fnol_data.get('complexity')} | |
| Is sufficient information available to proceed to settlement? | |
| If yes β recommend final settlement amount and target date. | |
| If no β list specifically what is still outstanding. | |
| Return ONLY JSON: | |
| {{"ready_to_settle":true,"final_reserve":0,"currency":"EUR","settlement_recommendation":"...","outstanding_items":["..."],"final_settlement_target_date":"...","rationale":"2-3 sentences"}}""" | |
| data, err = ai_json(prompt, system="You are a senior claims manager. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"ready_to_settle":False,"final_reserve":0,"currency":"EUR","settlement_recommendation":"Manual review required.","outstanding_items":["Surveyor report","Policy verification"],"final_settlement_target_date":"TBD","rationale":"Insufficient information for automated recommendation."}, "" | |
| def run_kpis(claim_data, fnol_data, reserve_data, routing_data, claim_type, org, open_date, settlement_date): | |
| prompt = f"""Generate KPI summary for a {claim_type} claim at {org or 'insurer'}. | |
| Claim opened: {open_date or 'Not specified'} | |
| Claim settled: {settlement_date or 'Not specified'} | |
| FNOL score: {fnol_data.get('estimated_claim_validity_score',50)}/100 | |
| Routing: {routing_data.get('routing_decision','STANDARD_REVIEW')} | |
| Reserve: β¬{reserve_data.get('reserve_expected',0):,} | |
| Return ONLY JSON: | |
| {{"total_days_to_settle":0,"sla_target_days":30,"sla_met":true,"stage_breakdown":[{{"stage":"...","days":0}}],"reserve_accuracy":"...","fraud_indicators_triggered":0,"customer_touchpoints":0,"performance_rating":"GREEN|AMBER|RED","performance_notes":"..."}}""" | |
| data, err = ai_json(prompt, system="You are a claims analytics expert. Return only valid JSON.") | |
| if data: return data, "" | |
| return {"total_days_to_settle":0,"sla_target_days":30,"sla_met":None,"stage_breakdown":[],"reserve_accuracy":"TBD","fraud_indicators_triggered":0,"customer_touchpoints":0,"performance_rating":"AMBER","performance_notes":"Insufficient data for KPI calculation."}, "" | |
| # βββ HTML RENDERERS βββββββββββββββββββββββββββββββββββββββββββ | |
| def render_claim_fields(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>No data yet.</div>" | |
| labels = [("claimant_name","Claimant"),("policy_number","Policy No."),("claim_reference","Reference"), | |
| ("claim_date","Claim Date"),("incident_date","Incident Date"),("claim_type","Type"), | |
| ("incident_location","Location"),("incident_description","Description"), | |
| ("claimant_contact","Contact"),("insurer_name","Insurer"), | |
| ("vehicle_registration","Vehicle Reg."),("third_party_vehicle_registration","Third Party Reg.")] | |
| html = "<div class='result-panel'><div class='result-panel-title'>π Extracted Claim Data</div><div class='field-grid'>" | |
| for k,label in labels: | |
| v = data.get(k) | |
| if v and str(v) not in ("null","None",""): | |
| html += f"<div class='field-key'>{label}</div><div class='field-val'>{v}</div>" | |
| html += "</div>" | |
| docs = [d for d in data.get("supporting_documents",[]) if d and d!="null"] | |
| if docs: | |
| html += "<div style='margin-top:12px;padding-top:10px;border-top:1px solid #eee'><span style='font-size:12px;font-weight:700;color:#555'>Documents: </span>" | |
| html += " Β· ".join(f"<span style='background:#e8f4fd;color:#0066cc;border-radius:4px;padding:2px 8px;font-size:12px'>{d}</span>" for d in docs) + "</div>" | |
| flags = [f for f in data.get("key_flags",[]) if f and f!="null"] | |
| if flags: | |
| html += "<div style='margin-top:8px'><span style='font-size:12px;font-weight:700;color:#FC5108'>β Flags: </span>" | |
| html += " Β· ".join(f"<span style='font-size:12px;color:#555'>{f}</span>" for f in flags) + "</div>" | |
| photo_findings = data.get("photo_findings") or [] | |
| if photo_findings: | |
| html += "<div style='margin-top:8px;padding-top:10px;border-top:1px solid #eee'><span style='font-size:12px;font-weight:700;color:#555'>π· Photo Analysis:</span>" | |
| for i, pf in enumerate(photo_findings, 1): | |
| desc = pf.get("damage_description","") | |
| plate = pf.get("number_plate_text") | |
| plate_html = f" β plate: <b style='color:#0066cc'>{plate}</b>" if plate and str(plate) not in ("null","None") else "" | |
| html += f"<div style='font-size:12px;color:#555;padding:3px 0'>Photo {i}: {desc}{plate_html}</div>" | |
| html += "</div>" | |
| html += "</div>" | |
| return html | |
| def render_fnol(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run FNOL Analysis first.</div>" | |
| validity = data.get("claim_validity","REQUIRES INVESTIGATION") | |
| score = data.get("estimated_claim_validity_score",50) | |
| summary = data.get("fnol_summary","") | |
| coverage = data.get("coverage_assessment",{}) | |
| checks = data.get("policy_checks",[]) | |
| questions = data.get("key_questions",[]) | |
| actions = data.get("immediate_actions",[]) | |
| escalate = data.get("escalation_needed",False) | |
| esc_rsn = data.get("escalation_reason","") | |
| complexity= data.get("complexity","MEDIUM") | |
| fault = data.get("fault_assessment",{}) | |
| v_color = {"VALID":"#155724","POTENTIALLY VALID":"#0c5460","REQUIRES INVESTIGATION":"#856404","POTENTIALLY INVALID":"#721c24"}.get(validity,"#856404") | |
| bar_col = "#28a745" if score>=70 else "#ffc107" if score>=40 else "#dc3545" | |
| cx_col = {"LOW":"#155724","MEDIUM":"#856404","HIGH":"#721c24"}.get(complexity,"#856404") | |
| cx_bg = {"LOW":"#d4edda","MEDIUM":"#fff3cd","HIGH":"#f8d7da"}.get(complexity,"#fff3cd") | |
| cov_icon= "β " if coverage.get("likely_covered")==True else "β" if coverage.get("likely_covered")==False else "β" | |
| cov_cls = "coverage-banner-yes" if coverage.get("likely_covered")==True else "coverage-banner-no" if coverage.get("likely_covered")==False else "coverage-banner-unk" | |
| fault_pct = fault.get("probability_not_at_fault", 50) | |
| fault_col = "#155724" if fault_pct >= 70 else "#856404" if fault_pct >= 40 else "#721c24" | |
| fault_bg = "#d4edda" if fault_pct >= 70 else "#fff3cd" if fault_pct >= 40 else "#f8d7da" | |
| html = f"""<div class='result-panel'> | |
| <div class='result-panel-title'>π FNOL Assessment</div> | |
| <div style='font-size:13px;color:#444;line-height:1.6;margin-bottom:16px'>{summary}</div> | |
| <div class='stat-row'> | |
| <div class='stat-box'><div class='stat-num' style='color:{v_color};font-size:15px'>{validity}</div><div class='stat-lbl'>Claim validity</div></div> | |
| <div class='stat-box'> | |
| <div class='stat-num' style='color:#FC5108'>{score}<span style='font-size:16px'>/100</span></div> | |
| <div style='background:#eee;border-radius:4px;height:6px;margin:6px 0'><div style='width:{score}%;background:{bar_col};height:100%;border-radius:4px'></div></div> | |
| <div class='stat-lbl'>Validity score</div> | |
| </div> | |
| <div class='stat-box'><div style='display:inline-block;background:{cx_bg};color:{cx_col};padding:4px 12px;border-radius:6px;font-size:14px;font-weight:700'>{complexity}</div><div class='stat-lbl' style='margin-top:6px'>Complexity</div></div> | |
| </div> | |
| <div class='fault-banner'> | |
| <div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>βοΈ Fault Assessment</div> | |
| <div style='display:flex;align-items:center;gap:16px'> | |
| <div style='background:{fault_bg};color:{fault_col};border-radius:8px;padding:10px 20px;text-align:center'> | |
| <div style='font-size:28px;font-weight:700;color:{fault_col}'>{fault_pct}%</div> | |
| <div style='font-size:11px;text-transform:uppercase;letter-spacing:.4px'>Probability insured is NOT at fault</div> | |
| </div> | |
| <div style='font-size:13px;color:#555;flex:1'>{fault.get("rationale","")}</div> | |
| </div> | |
| </div> | |
| <div class='{cov_cls}'><b>{cov_icon} Coverage: {coverage.get("coverage_confidence","N/A")} confidence</b> β {coverage.get("coverage_notes","")}</div>""" | |
| vm = data.get("vehicle_match", {}) | |
| vm_status = vm.get("status") | |
| if vm_status and vm_status != "NOT_APPLICABLE": | |
| vm_icon = {"MATCH":"β ","MISMATCH":"π¨","NOT_DETECTED":"β","NO_PHOTO_PROVIDED":"π·"}.get(vm_status,"β") | |
| vm_cls = {"MATCH":"coverage-banner-yes","MISMATCH":"coverage-banner-no","NOT_DETECTED":"coverage-banner-unk","NO_PHOTO_PROVIDED":"coverage-banner-unk"}.get(vm_status,"coverage-banner-unk") | |
| vm_label = {"MATCH":"Number plate matches","MISMATCH":"Number plate MISMATCH","NOT_DETECTED":"No plate detected in photos","NO_PHOTO_PROVIDED":"No damage photo provided"}.get(vm_status, vm_status) | |
| claim_reg = vm.get("claim_vehicle_registration") or "β" | |
| photo_plates = [p for p in (vm.get("photo_plate_numbers") or []) if p] | |
| photo_plates_str = ", ".join(photo_plates) if photo_plates else "none detected" | |
| html += f"<div class='{vm_cls}'><b>{vm_icon} Vehicle Match: {vm_label}</b><br>Claim reg: <b>{claim_reg}</b> Β· Photo plate(s): <b>{photo_plates_str}</b></div>" | |
| if escalate and esc_rsn: | |
| html += f"<div class='escalation-banner'>π¨ Escalation Required: {esc_rsn}</div>" | |
| passes = sum(1 for c in checks if c.get("status")=="PASS") | |
| fails = sum(1 for c in checks if c.get("status")=="FAIL") | |
| warns = sum(1 for c in checks if c.get("status")=="WARNING") | |
| html += f"<div style='font-size:13px;font-weight:700;color:#0E2841;margin:14px 0 8px'>π Policy Condition Checks</div>" | |
| html += f"<div style='font-size:12px;color:#555;margin-bottom:10px'>β {passes} passed β {fails} failed β οΈ {warns} warnings</div>" | |
| cls_map = {"PASS":"policy-pass","FAIL":"policy-fail","WARNING":"policy-warn","UNKNOWN":"policy-unknown"} | |
| icon_map = {"PASS":"β ","FAIL":"β","WARNING":"β οΈ","UNKNOWN":"β"} | |
| for c in checks: | |
| st = c.get("status","UNKNOWN") | |
| html += f"<div class='{cls_map.get(st,'policy-unknown')}'><div style='font-size:12px;font-weight:700;margin-bottom:3px'>{icon_map.get(st,'β')} {st} β {c.get('rule','')}</div><div style='font-size:12px;opacity:.9'>{c.get('finding','')}</div></div>" | |
| if questions: | |
| html += "<div style='margin-top:14px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>β Key Questions for Claimant</div>" | |
| html += "".join(f"<div style='font-size:12px;color:#444;padding:3px 0'>β’ {q}</div>" for q in questions) + "</div>" | |
| # Immediate actions β filter out the removed items | |
| REMOVE_ACTIONS = [ | |
| "second repair estimate", "witness statement from sarah clarke", | |
| "cityauto repairs", "approved repairer", "non-approved repairer" | |
| ] | |
| if actions: | |
| filtered = [a for a in actions if not any(kw in a.lower() for kw in REMOVE_ACTIONS)] | |
| if filtered: | |
| html += "<div style='margin-top:10px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>β‘ Immediate Actions Required</div>" | |
| html += "".join(f"<div style='font-size:12px;color:#444;padding:3px 0'>β {a}</div>" for a in filtered) + "</div>" | |
| html += "</div>" | |
| return html | |
| def render_reserve(data, claim_type): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run Reserve Recommendation first.</div>" | |
| mid = data.get("reserve_expected", 0) | |
| conf = data.get("confidence","MEDIUM") | |
| conf_color = {"HIGH":"#155724","MEDIUM":"#856404","LOW":"#721c24"}.get(conf,"#856404") | |
| conf_bg = {"HIGH":"#d4edda","MEDIUM":"#fff3cd","LOW":"#f8d7da"}.get(conf,"#fff3cd") | |
| html = f"""<div class='reserve-box'> | |
| <div style='font-size:14px;font-weight:700;color:#0E2841;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #eee'> | |
| π° Reserve Recommendation β {claim_type} | |
| <span style='float:right;background:{conf_bg};color:{conf_color};padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700'>{conf} CONFIDENCE</span> | |
| </div> | |
| <div style='background:#fff8e1;border:1px solid #ffc107;border-radius:8px;padding:12px 16px;margin-bottom:12px;font-size:13px;color:#856404'> | |
| <b>βΉοΈ Note:</b> An initial reserve estimate is uploaded per claim type and reviewed by the surveyor. The final repair amount is determined solely by the surveyor β not self-reported. | |
| </div> | |
| <div class='reserve-grid'> | |
| <div class='reserve-col reserve-mid'><div class='reserve-label'>Expected Reserve</div><div class='reserve-amount'>β¬{mid:,}</div><div style='font-size:11px;color:#856404;margin-top:3px'>Subject to surveyor confirmation</div></div> | |
| </div> | |
| <div style='font-size:13px;color:#444;line-height:1.6;margin-bottom:12px'>{data.get("rationale","")}</div>""" | |
| if data.get("key_drivers"): | |
| html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#333;margin-bottom:5px'>π Key Reserve Drivers</div>" | |
| html += "".join(f"<span style='background:#e8f4fd;color:#0066cc;border-radius:4px;padding:3px 10px;font-size:12px;margin:2px;display:inline-block'>{d}</span>" for d in data["key_drivers"]) + "</div>" | |
| triggers = data.get("adjustment_triggers", [ | |
| "Legal proceedings initiated", | |
| "Medical complications arise", | |
| "Third party injury claims submitted", | |
| "Evidence of underinsurance identified", | |
| "Liability remains disputed after investigation", | |
| "Repair costs exceed initial surveyor estimate" | |
| ]) | |
| html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#721c24;margin-bottom:5px'>β Reserve Increase Triggers</div>" | |
| html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β’ {t}</div>" for t in triggers) + "</div>" | |
| review = data.get("recommended_review", "Minimum 30 days from today") | |
| html += f"<div style='font-size:12px;color:#555;padding:8px 12px;background:#f8f9fa;border-radius:6px;margin-bottom:8px'><b>Next reserve review:</b> {review}</div>" | |
| if data.get("ibnr_note"): | |
| html += f"<div style='font-size:12px;color:#555;padding:8px 12px;background:#f8f9fa;border-radius:6px'><b>IBNR note:</b> {data['ibnr_note']}</div>" | |
| html += "</div>" | |
| return html | |
| def render_fraud(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run Risk Detection first.</div>" | |
| level = data.get("fraud_risk_level","MEDIUM").upper() | |
| score = data.get("risk_score",50) | |
| action = data.get("recommended_action","REVIEW") | |
| cls = {"LOW":"fraud-low","MEDIUM":"fraud-medium","HIGH":"fraud-high"}.get(level,"fraud-medium") | |
| icon = {"LOW":"π’","MEDIUM":"π‘","HIGH":"π΄"}.get(level,"π‘") | |
| a_col = {"APPROVE":"chip-approve","REVIEW":"chip-review","INVESTIGATE":"chip-investigate","DECLINE":"chip-decline"}.get(action,"chip-review") | |
| bar_col= "#28a745" if score<35 else "#ffc107" if score<65 else "#dc3545" | |
| fnol_c = data.get("fnol_consistency","") | |
| fc_col = {"CONSISTENT":"#155724","INCONSISTENT":"#721c24","PARTIALLY CONSISTENT":"#856404"}.get(fnol_c,"#555") | |
| html = f"""<div class='{cls}'> | |
| <div style='font-size:17px;font-weight:700;margin-bottom:12px'>{icon} Risk Level: <b>{level}</b></div> | |
| <div style='background:rgba(0,0,0,0.08);border-radius:4px;height:10px;overflow:hidden;margin-bottom:10px'> | |
| <div style='width:{score}%;background:{bar_col};height:100%;border-radius:4px'></div> | |
| </div> | |
| <div style='font-size:12px;color:#555;margin-bottom:12px'>Risk score: <b>{score}/100</b></div> | |
| <div style='margin-bottom:12px'><span class='action-chip {a_col}'>β‘ {action}</span><span style='font-size:13px;color:#555;margin-left:10px'>{data.get("action_reason","")}</span></div>""" | |
| if fnol_c: | |
| html += f"<div style='font-size:12px;margin-bottom:12px;padding:8px 12px;background:rgba(255,255,255,0.5);border-radius:6px'><b>FNOL Consistency:</b> <span style='color:{fc_col};font-weight:700'>{fnol_c}</span> β {data.get('fnol_notes','')}</div>" | |
| for title, key, col in [("π© Red Flags","red_flags","#721c24"),("β οΈ Risk Factors","risk_factors","#856404"),("β Legitimacy Indicators","positive_indicators","#155724"),("π Data Gaps","data_gaps","#444")]: | |
| items = data.get(key,[]) | |
| if items: | |
| html += f"<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:{col};margin-bottom:4px'>{title}</div>" | |
| html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β’ {i}</div>" for i in items) + "</div>" | |
| html += "</div>" | |
| return html | |
| def render_routing(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run Adjudication Routing first.</div>" | |
| decision = data.get("routing_decision","STANDARD_REVIEW") | |
| conf = data.get("confidence_score",60) | |
| cls_map = {"STRAIGHT_TO_PAYMENT":"routing-approve","FAST_TRACK_REVIEW":"routing-review","STANDARD_REVIEW":"routing-review","FULL_INVESTIGATION":"routing-investigate","DECLINE":"routing-decline"} | |
| lbl_map = {"STRAIGHT_TO_PAYMENT":"β Straight to Payment","FAST_TRACK_REVIEW":"β‘ Fast Track Review","STANDARD_REVIEW":"π Standard Review","FULL_INVESTIGATION":"π Full Investigation","DECLINE":"β Decline"} | |
| bar_col = "#28a745" if conf>=70 else "#ffc107" if conf>=40 else "#dc3545" | |
| html = f"""<div class='{cls_map.get(decision,"routing-review")}'> | |
| <div style='font-size:18px;font-weight:700;margin-bottom:8px'>{lbl_map.get(decision,decision)}</div> | |
| <div style='background:rgba(0,0,0,0.1);border-radius:4px;height:8px;margin-bottom:6px'><div style='width:{conf}%;background:{bar_col};height:100%;border-radius:4px'></div></div> | |
| <div style='font-size:12px;color:#555;margin-bottom:12px'>Confidence: <b>{conf}%</b></div> | |
| <div style='font-size:13px;color:#333;line-height:1.6;margin-bottom:14px'>{data.get("decision_rationale","")}</div> | |
| <div style='display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px'> | |
| <div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:22px;font-weight:700;color:#0E2841'>{data.get("estimated_settlement_days",14)}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Est. days to settle</div></div> | |
| <div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:13px;font-weight:700;color:#0E2841'>{data.get("settlement_amount_recommendation","Per adjuster")}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Settlement amount</div></div> | |
| <div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:12px;font-weight:700;color:#0E2841'>{data.get("next_handler","")}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Assigned to</div></div> | |
| </div>""" | |
| factors = data.get("decision_factors",[]) | |
| if factors: | |
| html += "<div style='margin-bottom:12px'><div style='font-size:12px;font-weight:700;color:#333;margin-bottom:6px'>Decision Factors</div>" | |
| for f in factors: | |
| ic = {"POSITIVE":"β ","NEGATIVE":"β","NEUTRAL":"β‘οΈ"}.get(f.get("impact","NEUTRAL"),"β‘οΈ") | |
| html += f"<div style='font-size:12px;color:#444;padding:3px 0'>{ic} {f.get('factor','')}</div>" | |
| html += "</div>" | |
| conds = [c for c in data.get("conditions",[]) if c and c!="None"] | |
| if conds: | |
| html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#555;margin-bottom:4px'>π Conditions Before Payment</div>" | |
| html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β’ {c}</div>" for c in conds) + "</div>" | |
| if data.get("audit_trail"): | |
| html += f"<div style='font-size:11px;color:#777;padding:8px 12px;background:rgba(255,255,255,0.5);border-radius:6px'><b>Audit trail:</b> {data['audit_trail']}</div>" | |
| html += "</div>" | |
| return html | |
| def render_survey(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run Survey first.</div>" | |
| estimate = data.get("surveyor_cost_estimate", 0) | |
| html = f"""<div class='result-panel'> | |
| <div class='result-panel-title'>π§ Surveyor Assessment</div> | |
| <div style='background:#fff8e1;border:1px solid #ffc107;border-radius:8px;padding:10px 14px;margin-bottom:12px;font-size:13px;color:#856404'> | |
| <b>βΉοΈ Note:</b> Repair/settlement amount is determined solely by the surveyor. | |
| </div> | |
| <div style='font-size:13px;color:#444;margin-bottom:10px'><b>Inspection Findings:</b> {data.get("inspection_findings","")}</div> | |
| <div style='font-size:13px;color:#444;margin-bottom:10px'><b>Damage Description:</b> {data.get("damage_description","")}</div> | |
| <div style='font-size:13px;color:#444;margin-bottom:10px'><b>Repair Recommendation:</b> {data.get("surveyor_repair_recommendation","")}</div> | |
| <div style='background:#f0fdf4;border:1.5px solid #16a34a;border-radius:8px;padding:14px;text-align:center;margin:12px 0'> | |
| <div style='font-size:28px;font-weight:700;color:#155724'>β¬{estimate:,}</div> | |
| <div style='font-size:12px;color:#155724;text-transform:uppercase;letter-spacing:.4px'>Surveyor Cost Estimate</div> | |
| </div> | |
| <div style='font-size:13px;color:#444;margin-bottom:8px'><b>Overall Assessment:</b> {data.get("surveyor_overall_assessment","")}</div>""" | |
| discrep = data.get("discrepancies",[]) | |
| if discrep: | |
| html += "<div style='margin-top:8px'><b style='font-size:12px;color:#721c24'>Discrepancies noted:</b>" | |
| html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β’ {d}</div>" for d in discrep) + "</div>" | |
| html += "</div>" | |
| return html | |
| def render_final_reserve(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run Final Reserve first.</div>" | |
| ready = data.get("ready_to_settle", False) | |
| final = data.get("final_reserve", 0) | |
| target = data.get("final_settlement_target_date","TBD") | |
| cls = "routing-approve" if ready else "routing-investigate" | |
| icon = "β Ready to Proceed to Payment" if ready else "β³ Additional Information Required" | |
| html = f"""<div class='{cls}'> | |
| <div style='font-size:17px;font-weight:700;margin-bottom:10px'>{icon}</div> | |
| <div style='font-size:13px;color:#333;margin-bottom:12px'>{data.get("rationale","")}</div>""" | |
| if ready: | |
| html += f"""<div style='display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px'> | |
| <div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:14px;text-align:center'> | |
| <div style='font-size:26px;font-weight:700;color:#155724'>β¬{final:,}</div> | |
| <div style='font-size:11px;color:#555;text-transform:uppercase'>Final Settlement Amount</div> | |
| </div> | |
| <div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:14px;text-align:center'> | |
| <div style='font-size:16px;font-weight:700;color:#0E2841'>{target}</div> | |
| <div style='font-size:11px;color:#555;text-transform:uppercase'>Final Settlement Target Date</div> | |
| </div> | |
| </div> | |
| <div style='font-size:13px;color:#333'><b>Recommendation:</b> {data.get("settlement_recommendation","")}</div>""" | |
| else: | |
| outstanding = data.get("outstanding_items",[]) | |
| if outstanding: | |
| html += "<div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>Outstanding items before settlement:</div>" | |
| html += "".join(f"<div style='font-size:13px;color:#555;padding:3px 0'>β {item}</div>" for item in outstanding) | |
| html += "</div>" | |
| return html | |
| def render_kpis(data): | |
| if not data: return "<div style='color:#aaa;padding:16px'>Run KPI report first.</div>" | |
| total = data.get("total_days_to_settle", 0) | |
| sla = data.get("sla_target_days", 30) | |
| met = data.get("sla_met") | |
| rating= data.get("performance_rating","AMBER") | |
| r_col = {"GREEN":"#155724","AMBER":"#856404","RED":"#721c24"}.get(rating,"#856404") | |
| r_bg = {"GREEN":"#d4edda","AMBER":"#fff3cd","RED":"#f8d7da"}.get(rating,"#fff3cd") | |
| sla_icon = "β " if met else "β" if met is False else "β" | |
| html = f"""<div class='result-panel'> | |
| <div class='result-panel-title'>π Claims KPI Dashboard</div> | |
| <div style='display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px'> | |
| <div class='stat-box'><div class='stat-num' style='color:#0E2841'>{total}</div><div class='stat-lbl'>Days to Settle</div></div> | |
| <div class='stat-box'><div class='stat-num' style='color:#555'>{sla}</div><div class='stat-lbl'>SLA Target (days)</div></div> | |
| <div class='stat-box'><div class='stat-num'>{sla_icon}</div><div class='stat-lbl'>SLA Met</div></div> | |
| <div class='stat-box'><div style='background:{r_bg};color:{r_col};border-radius:6px;padding:4px 10px;font-size:14px;font-weight:700;display:inline-block'>{rating}</div><div class='stat-lbl' style='margin-top:6px'>Performance</div></div> | |
| </div>""" | |
| stages = data.get("stage_breakdown",[]) | |
| if stages: | |
| html += "<div style='margin-bottom:14px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:8px'>Stage Breakdown</div>" | |
| for s in stages: | |
| d = s.get("days",0) | |
| bar_w = min(100, int(d/max(total,1)*100)) if total else 0 | |
| html += f"""<div style='margin-bottom:6px'> | |
| <div style='display:flex;justify-content:space-between;font-size:12px;color:#555;margin-bottom:2px'><span>{s.get("stage","")}</span><span><b>{d} days</b></span></div> | |
| <div style='background:#eee;border-radius:4px;height:6px'><div style='width:{bar_w}%;background:#4a90d9;height:100%;border-radius:4px'></div></div> | |
| </div>""" | |
| html += "</div>" | |
| extras = [("Reserve Accuracy","reserve_accuracy"),("Fraud Indicators Triggered","fraud_indicators_triggered"),("Customer Touchpoints","customer_touchpoints")] | |
| for label, key in extras: | |
| v = data.get(key) | |
| if v is not None: | |
| html += f"<div style='font-size:12px;color:#555;padding:4px 0;border-bottom:1px solid #f0f0f0'><b>{label}:</b> {v}</div>" | |
| if data.get("performance_notes"): | |
| html += f"<div style='font-size:13px;color:#555;margin-top:12px;padding:10px 14px;background:#f8f9fa;border-radius:6px'>{data['performance_notes']}</div>" | |
| html += "</div>" | |
| return html | |
| # βββ BUILD APP ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_app(): | |
| with gr.Blocks(title="Claims AI Diagnostic") as demo: | |
| s_ct = gr.State("Motor β Third Party (Bumper to Bumper)") | |
| s_org = gr.State("") | |
| s_raw = gr.State("") | |
| s_extract = gr.State({}) | |
| s_fnol = gr.State({}) | |
| s_reserve = gr.State({}) | |
| s_fraud = gr.State({}) | |
| s_routing = gr.State({}) | |
| s_survey = gr.State({}) | |
| s_final_r = gr.State({}) | |
| gr.HTML("""<div style='background:linear-gradient(135deg,#0E2841 0%,#1a3d5c 100%);border-radius:12px;padding:24px 28px 20px;margin-bottom:4px;border-bottom:3px solid #FC5108'> | |
| <div style='display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px'> | |
| <div> | |
| <div style='font-size:24px;font-weight:700;color:#ffffff;margin-bottom:4px'>π Claims AI Diagnostic</div> | |
| <div style='font-size:13px;color:#A8BDD6'>AI-powered claims assessment Β· FNOL Β· Policy review Β· Reserve Β· Fraud detection Β· Adjudication</div> | |
| </div> | |
| <div style='background:rgba(252,81,8,0.2);border:1px solid #FC5108;color:#FC5108;border-radius:6px;padding:5px 14px;font-size:12px;font-weight:600'>POC Demo β PwC Malta</div> | |
| </div> | |
| </div>""") | |
| with gr.Tabs() as tabs: | |
| # ββββ STEP 1: Claims Type ββββ | |
| with gr.Tab("β Claims Type", id=0): | |
| gr.HTML("<div class='info-bar'><b>Welcome to the Claims AI Diagnostic.</b> Select your claims type and organisation, then upload a real claim document or proceed to document extraction.</div>") | |
| gr.HTML("<div class='step-title'>Select Claims Type</div><div class='step-sub'>Motor claims β choose your line of business</div>") | |
| ct_dd = gr.Dropdown(choices=CLAIMS_TYPES, value="Motor β Third Party (Bumper to Bumper)", label="Claims Type") | |
| org_input = gr.Textbox(label="Organisation Name", placeholder="Enter your organisation name") | |
| with gr.Row(): | |
| gr.HTML("") | |
| next1 = gr.Button("Next: Upload Document β", variant="primary") | |
| # ββββ STEP 2: Document Extraction ββββ | |
| with gr.Tab("β‘ Document Extraction", id=1): | |
| gr.HTML("<div class='info-bar'>For the purposes of the PwC demo β upload a claim form and any supporting documents. Claude AI will extract all key fields automatically. For <b>Motor β Third Party (Bumper to Bumper)</b> claims, also upload Malta's Bumper to Bumper accident report form if available, plus any photos of damage.</div>") | |
| gr.HTML("<div class='step-title'>Upload & Extract Claim Document</div><div class='step-sub'>PDF, JPG, PNG β AI reads and extracts every visible field</div>") | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| doc_file = gr.File(label="π Claim document (PDF, JPG, PNG)", file_types=[".pdf",".jpg",".jpeg",".png",".txt"], type="filepath") | |
| bumper_form_file = gr.File(label="π Bumper to Bumper Form (Malta β Third Party claims)", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath") | |
| damage_photos = gr.File(label="π· Photos of damage (multiple)", file_types=[".jpg",".jpeg",".png",".webp"], file_count="multiple", type="filepath") | |
| with gr.Column(scale=2): | |
| policy_file = gr.File(label="π Policy document (for the purposes of the PwC demo)", file_types=[".pdf",".txt"], type="filepath") | |
| extract_btn = gr.Button("π€ Extract Claim Data with AI", variant="primary") | |
| extract_status = gr.HTML("") | |
| extract_out = gr.HTML("<div style='color:#aaa;padding:16px;font-size:13px'>Upload a document and click Extract.</div>") | |
| def do_extract(fp, ct, org, bumper_fp, damage_fps): | |
| if not fp and not bumper_fp and not damage_fps: | |
| yield render_claim_fields(None), "<div style='color:orange'>β οΈ Please upload a document.</div>", {}, "" | |
| return | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Extracting document(s)...</div>", {}, "" | |
| data, raw, err = analyse_document(fp, ct, org, bumper_fp, damage_fps) | |
| if err and not data: | |
| yield render_claim_fields(None), f"<div style='color:red'>β {err}</div>", {}, "" | |
| return | |
| yield render_claim_fields(data), "<div style='color:green;font-size:13px'>β Extraction complete β proceed to FNOL Analysis.</div>", data or {}, raw or "" | |
| extract_btn.click(do_extract, inputs=[doc_file, ct_dd, org_input, bumper_form_file, damage_photos], outputs=[extract_out, extract_status, s_extract, s_raw]) | |
| def toggle_bumper(ct): | |
| return gr.update(visible=(ct == "Motor β Third Party (Bumper to Bumper)")) | |
| ct_dd.change(toggle_bumper, inputs=ct_dd, outputs=bumper_form_file) | |
| with gr.Row(): | |
| back2 = gr.Button("β Back"); next2 = gr.Button("Next: FNOL & Policy Review β", variant="primary") | |
| # ββββ STEP 3: FNOL ββββ | |
| with gr.Tab("β’ FNOL & Policy Review", id=2): | |
| gr.HTML("<div class='info-bar'><b>First Notice of Loss assessment.</b> AI reviews the claim against every policy condition, assigns a validity score, determines coverage, and assesses fault probability.</div>") | |
| gr.HTML("<div class='step-title'>FNOL Analysis & Policy Comparison</div>") | |
| fnol_btn = gr.Button("π Run FNOL Analysis", variant="primary") | |
| fnol_status = gr.HTML("") | |
| fnol_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run FNOL Analysis.</div>") | |
| def do_fnol(ed, raw, ct, org, pf): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Running FNOL assessment...</div>" | |
| data, err = run_fnol_policy(ed, raw, ct, org, pf) | |
| yield render_fnol(data), "<div style='color:green;font-size:13px'>β FNOL complete.</div>" | |
| fnol_btn.click(do_fnol, inputs=[s_extract,s_raw,ct_dd,org_input,policy_file], outputs=[fnol_out, fnol_status]) | |
| fnol_btn.click(lambda ed,raw,ct,org,pf: run_fnol_policy(ed,raw,ct,org,pf)[0] or {}, inputs=[s_extract,s_raw,ct_dd,org_input,policy_file], outputs=s_fnol) | |
| with gr.Row(): | |
| back3 = gr.Button("β Back"); next3 = gr.Button("Next: Reserve β", variant="primary") | |
| # ββββ STEP 4: Reserve ββββ | |
| with gr.Tab("β£ Reserve Recommendation", id=3): | |
| gr.HTML("<div class='info-bar'><b>Initial reserve setting.</b> An initial reserve estimate is uploaded per claim type and reviewed by the surveyor. The expected reserve is shown below β final amount is subject to surveyor confirmation.</div>") | |
| gr.HTML("<div class='step-title'>Claims Reserve Recommendation</div>") | |
| reserve_btn = gr.Button("π° Generate Reserve Recommendation", variant="primary") | |
| reserve_status = gr.HTML("") | |
| reserve_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Generate Reserve Recommendation.</div>") | |
| def do_reserve(ed, fd, ct, org): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Calculating reserve...</div>" | |
| data, err = run_reserve(ed, fd, ct, org) | |
| yield render_reserve(data, ct), "<div style='color:green;font-size:13px'>β Reserve complete.</div>" | |
| reserve_btn.click(do_reserve, inputs=[s_extract,s_fnol,ct_dd,org_input], outputs=[reserve_out, reserve_status]) | |
| reserve_btn.click(lambda ed,fd,ct,org: run_reserve(ed,fd,ct,org)[0] or {}, inputs=[s_extract,s_fnol,ct_dd,org_input], outputs=s_reserve) | |
| with gr.Row(): | |
| back4 = gr.Button("β Back"); next4 = gr.Button("Next: Risk Detection β", variant="primary") | |
| # ββββ STEP 5: Risk Detection ββββ | |
| with gr.Tab("β€ Risk Detection", id=4): | |
| gr.HTML("<div class='info-bar'><b>AI risk analysis cross-referencing FNOL findings.</b> Checks for red flags, inconsistencies, and fraud indicators.</div>") | |
| gr.HTML("<div class='step-title'>Risk Assessment</div>") | |
| fraud_btn = gr.Button("π Run Risk Detection", variant="primary") | |
| fraud_status = gr.HTML("") | |
| fraud_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Risk Detection.</div>") | |
| def do_fraud(ed, fd, ct): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Analysing risk indicators...</div>" | |
| data, err = run_fraud(ed, fd, ct) | |
| yield render_fraud(data), "<div style='color:green;font-size:13px'>β Risk analysis complete.</div>" | |
| fraud_btn.click(do_fraud, inputs=[s_extract,s_fnol,s_ct], outputs=[fraud_out, fraud_status]) | |
| fraud_btn.click(lambda ed,fd,ct: run_fraud(ed,fd,ct)[0] or {}, inputs=[s_extract,s_fnol,s_ct], outputs=s_fraud) | |
| with gr.Row(): | |
| back5 = gr.Button("β Back"); next5 = gr.Button("Next: Adjudication β", variant="primary") | |
| # ββββ STEP 6: Adjudication ββββ | |
| with gr.Tab("β₯ Adjudication & Routing", id=5): | |
| gr.HTML("<div class='info-bar'><b>Automated adjudication decision.</b> AI evaluates all prior findings and recommends a payment routing with rationale and audit trail.</div>") | |
| gr.HTML("<div class='step-title'>Adjudication & Payment Routing</div>") | |
| routing_btn = gr.Button("βοΈ Run Adjudication Decision", variant="primary") | |
| routing_status = gr.HTML("") | |
| routing_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Adjudication Decision.</div>") | |
| def do_routing(ed, fd, fraud_d, res_d, ct, org): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Running adjudication...</div>" | |
| data, err = run_routing(ed, fd, fraud_d, res_d, ct, org) | |
| yield render_routing(data), "<div style='color:green;font-size:13px'>β Routing decision complete.</div>" | |
| routing_btn.click(do_routing, inputs=[s_extract,s_fnol,s_fraud,s_reserve,s_ct,s_org], outputs=[routing_out, routing_status]) | |
| routing_btn.click(lambda ed,fd,fraud_d,res_d,ct,org: run_routing(ed,fd,fraud_d,res_d,ct,org)[0] or {}, inputs=[s_extract,s_fnol,s_fraud,s_reserve,s_ct,s_org], outputs=s_routing) | |
| with gr.Row(): | |
| back6 = gr.Button("β Back"); next6 = gr.Button("Next: Survey β", variant="primary") | |
| # ββββ STEP 7: Survey ββββ | |
| with gr.Tab("β¦ Survey", id=6): | |
| gr.HTML("<div class='info-bar'><b>Surveyor inspection.</b> Upload the surveyor report if available. The repair/settlement amount is determined solely by the surveyor β not self-reported by the claimant.</div>") | |
| gr.HTML("<div class='step-title'>Surveyor Assessment</div>") | |
| survey_file = gr.File(label="π Upload Surveyor Report (optional)", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath") | |
| survey_btn = gr.Button("π§ Run Survey Assessment", variant="primary") | |
| survey_status = gr.HTML("") | |
| survey_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Survey Assessment.</div>") | |
| def do_survey(ed, fd, ct, org, sf): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Running survey assessment...</div>" | |
| data, err = run_survey(ed, fd, ct, org, sf) | |
| yield render_survey(data), "<div style='color:green;font-size:13px'>β Survey complete.</div>" | |
| survey_btn.click(do_survey, inputs=[s_extract,s_fnol,ct_dd,org_input,survey_file], outputs=[survey_out, survey_status]) | |
| survey_btn.click(lambda ed,fd,ct,org,sf: run_survey(ed,fd,ct,org,sf)[0] or {}, inputs=[s_extract,s_fnol,ct_dd,org_input,survey_file], outputs=s_survey) | |
| with gr.Row(): | |
| back7 = gr.Button("β Back"); next7 = gr.Button("Next: Final Reserve β", variant="primary") | |
| # ββββ STEP 8: Final Reserve ββββ | |
| with gr.Tab("β§ Final Reserve Recommendation", id=7): | |
| gr.HTML("<div class='info-bar'><b>Final reserve recommendation.</b> Based on surveyor findings, AI recommends whether to proceed to payment or request additional information, with a final settlement target date.</div>") | |
| gr.HTML("<div class='step-title'>Final Reserve & Settlement Decision</div>") | |
| fr_btn = gr.Button("π Generate Final Recommendation", variant="primary") | |
| fr_status = gr.HTML("") | |
| fr_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Generate Final Recommendation.</div>") | |
| def do_final_reserve(ed, fd, res_d, sur_d, ct, org): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Generating final recommendation...</div>" | |
| data, err = run_final_reserve(ed, fd, res_d, sur_d, ct, org) | |
| yield render_final_reserve(data), "<div style='color:green;font-size:13px'>β Final recommendation complete.</div>" | |
| fr_btn.click(do_final_reserve, inputs=[s_extract,s_fnol,s_reserve,s_survey,ct_dd,org_input], outputs=[fr_out, fr_status]) | |
| fr_btn.click(lambda ed,fd,res_d,sur_d,ct,org: run_final_reserve(ed,fd,res_d,sur_d,ct,org)[0] or {}, inputs=[s_extract,s_fnol,s_reserve,s_survey,ct_dd,org_input], outputs=s_final_r) | |
| with gr.Row(): | |
| back8 = gr.Button("β Back"); next8 = gr.Button("Next: KPIs β", variant="primary") | |
| # ββββ STEP 9: KPIs ββββ | |
| with gr.Tab("β¨ KPIs & Analytics", id=8): | |
| gr.HTML("<div class='info-bar'><b>Claim performance metrics.</b> Enter claim open and settlement dates to generate SLA compliance, stage breakdown, and overall performance rating.</div>") | |
| gr.HTML("<div class='step-title'>KPI Dashboard</div>") | |
| with gr.Row(): | |
| kpi_open = gr.Textbox(label="Claim Open Date", placeholder="e.g. 01 May 2026") | |
| kpi_close = gr.Textbox(label="Settlement Date", placeholder="e.g. 10 June 2026") | |
| kpi_btn = gr.Button("π Generate KPI Report", variant="primary") | |
| kpi_status = gr.HTML("") | |
| kpi_out = gr.HTML("<div style='color:#aaa;padding:16px'>Enter dates and click Generate KPI Report.</div>") | |
| def do_kpis(ed, fd, res_d, rout_d, ct, org, od, sd): | |
| yield gr.update(), "<div style='color:#FC5108;font-size:13px'>β³ Calculating KPIs...</div>" | |
| data, err = run_kpis(ed, fd, res_d, rout_d, ct, org, od, sd) | |
| yield render_kpis(data), "<div style='color:green;font-size:13px'>β KPI report complete.</div>" | |
| kpi_btn.click(do_kpis, inputs=[s_extract,s_fnol,s_reserve,s_routing,ct_dd,org_input,kpi_open,kpi_close], outputs=[kpi_out, kpi_status]) | |
| with gr.Row(): | |
| back9 = gr.Button("β Back") | |
| # ββββ STEP 10: Demo Scenarios ββββ | |
| with gr.Tab("β© Demo Scenarios", id=9): | |
| gr.HTML("<div class='info-bar'><b>Pre-built demo scenarios for presentation purposes.</b> Click a scenario to pre-load data, then navigate through the tabs.</div>") | |
| gr.HTML("<div class='step-title'>Quick Demo Scenarios</div>") | |
| with gr.Row(): | |
| demo_btn1 = gr.Button("π Motor β Third Party (Bumper to Bumper)\nβ¬2,850 Β· Low complexity", variant="secondary") | |
| demo_btn2 = gr.Button("π Motor β Own Damage (Comprehensive)\nβ¬6,400 Β· Medium complexity", variant="secondary") | |
| demo_preview = gr.HTML("") | |
| def load_demo(key): | |
| d = DEMO_CLAIMS.get(key, {}) | |
| if not d: return key, gr.update(), {} | |
| html = f"""<div style='background:#f0f7ff;border:1.5px solid #4a90d9;border-radius:10px;padding:14px 18px;margin-top:14px'> | |
| <div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:8px'>β Demo loaded: {d.get('claim_type','')}</div> | |
| <div style='display:grid;grid-template-columns:repeat(3,1fr);gap:8px'> | |
| <div><div style='font-size:11px;color:#888'>Claimant</div><div style='font-size:13px;font-weight:600'>{d.get('claimant_name','')}</div></div> | |
| <div><div style='font-size:11px;color:#888'>Policy</div><div style='font-size:13px;font-weight:600'>{d.get('policy_number','')}</div></div> | |
| <div><div style='font-size:11px;color:#888'>Location</div><div style='font-size:13px;font-weight:600'>{d.get('incident_location','')}</div></div> | |
| </div> | |
| <div style='font-size:12px;color:#555;margin-top:8px'>{d.get('incident_description','')[:180]}...</div> | |
| <div style='font-size:12px;color:#FC5108;margin-top:6px'>β Data pre-loaded β go to β Claims Type tab to proceed</div> | |
| </div>""" | |
| return key, gr.HTML(html), d | |
| demo_btn1.click(lambda: load_demo("Motor β Third Party (Bumper to Bumper)"), outputs=[ct_dd, demo_preview, s_extract]) | |
| demo_btn2.click(lambda: load_demo("Motor β Own Damage (Comprehensive)"), outputs=[ct_dd, demo_preview, s_extract]) | |
| # ββ Navigation ββ | |
| def save1(ct, org): return ct, org, gr.update(selected=1) | |
| next1.click(save1, inputs=[ct_dd, org_input], outputs=[s_ct, s_org, tabs]) | |
| next2.click(lambda: gr.update(selected=2), outputs=tabs) | |
| back2.click(lambda: gr.update(selected=0), outputs=tabs) | |
| next3.click(lambda: gr.update(selected=3), outputs=tabs) | |
| back3.click(lambda: gr.update(selected=1), outputs=tabs) | |
| next4.click(lambda: gr.update(selected=4), outputs=tabs) | |
| back4.click(lambda: gr.update(selected=2), outputs=tabs) | |
| next5.click(lambda: gr.update(selected=5), outputs=tabs) | |
| back5.click(lambda: gr.update(selected=3), outputs=tabs) | |
| next6.click(lambda: gr.update(selected=6), outputs=tabs) | |
| back6.click(lambda: gr.update(selected=4), outputs=tabs) | |
| next7.click(lambda: gr.update(selected=7), outputs=tabs) | |
| back7.click(lambda: gr.update(selected=5), outputs=tabs) | |
| next8.click(lambda: gr.update(selected=8), outputs=tabs) | |
| back8.click(lambda: gr.update(selected=6), outputs=tabs) | |
| next8.click(lambda: gr.update(selected=8), outputs=tabs) | |
| back9.click(lambda: gr.update(selected=7), outputs=tabs) | |
| return demo | |
| if __name__ == "__main__": | |
| build_app().launch(css=CSS) |