Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="color-scheme" content="dark"> | |
| <title>Fraud Detection Analysis</title> | |
| <!-- Typography --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho+B1:wght@400;500;600;700&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| /* Force system controls (dropdowns) to dark mode */ | |
| /* Core Colors */ | |
| --clr-bg: #0800ff; | |
| /* Cobalt Blue */ | |
| --clr-text: #ffffff; | |
| --clr-divider: rgba(255, 255, 255, 0.2); | |
| /* Accents */ | |
| --clr-accent: rgba(255, 255, 255, 0.12); | |
| /* Soft White Accent */ | |
| --clr-highlight: rgba(255, 255, 255, 0.2); | |
| /* Mid Accent */ | |
| /* Spacing */ | |
| --space-unit: 20px; | |
| --tiers-gap: 60px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background-color: var(--clr-bg); | |
| color: var(--clr-text); | |
| font-family: 'Shippori Mincho B1', serif; | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| padding: 60px 20px; | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| .shine-hover { | |
| transition: all 0.3s ease; | |
| } | |
| .shine-hover:hover { | |
| background-color: rgba(255, 255, 255, 0.15) ; | |
| box-shadow: 0 0 10px rgba(255, 255, 255, 0.3); | |
| border-color: rgba(255, 255, 255, 0.8) ; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 720px; | |
| /* Editorial width */ | |
| } | |
| /* HEADER */ | |
| header { | |
| margin-bottom: 80px; | |
| text-align: center; | |
| } | |
| h1 { | |
| font-size: 32px; | |
| font-weight: 400; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 12px; | |
| } | |
| .subtitle { | |
| font-size: 14px; | |
| opacity: 0.8; | |
| font-weight: 400; | |
| letter-spacing: 0.02em; | |
| } | |
| /* CONFIG BAR */ | |
| .config-bar { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 40px; | |
| margin-bottom: 60px; | |
| /* Card Style */ | |
| background: rgba(255, 255, 255, 0.08); | |
| padding: 30px; | |
| border-radius: 6px; | |
| /* Standardized radius */ | |
| } | |
| /* TIERS */ | |
| .tier-section { | |
| margin-bottom: var(--tiers-gap); | |
| border-bottom: 1px solid var(--clr-divider); | |
| padding-bottom: 40px; | |
| } | |
| .tier-header { | |
| margin-bottom: 30px; | |
| display: flex; | |
| align-items: baseline; | |
| } | |
| .tier-title { | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| opacity: 0.8; | |
| } | |
| /* GRID */ | |
| .form-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 40px 30px; | |
| /* Row Gap, Column Gap */ | |
| } | |
| /* CONTROLS */ | |
| .control-group { | |
| margin-bottom: 0; | |
| position: relative; | |
| } | |
| label { | |
| display: block; | |
| font-size: 13px; | |
| margin-bottom: 12px; | |
| opacity: 0.9; | |
| } | |
| /* SLIDERS - Custom Minimal */ | |
| .slider-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| margin-bottom: 4px; | |
| /* Reduced from 8px for tighter spacing */ | |
| } | |
| .slider-val { | |
| font-size: 16px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 30px; | |
| /* Bigger hit area */ | |
| background: transparent; | |
| margin: 0; | |
| /* Centered by height */ | |
| cursor: pointer; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| input[type="range"]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 1px; | |
| background: rgba(255, 255, 255, 0.4); | |
| border: none; | |
| cursor: pointer; | |
| /* Clickable track */ | |
| transition: background 0.2s; | |
| } | |
| /* Subtle track hover feedback */ | |
| input[type="range"]:hover::-webkit-slider-runnable-track { | |
| background: rgba(255, 255, 255, 0.55); | |
| } | |
| /* Smaller, lighter thumb */ | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 12px; | |
| /* Even smaller thumb */ | |
| width: 12px; | |
| border-radius: 50%; | |
| background: var(--clr-text); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| /* Centered: half of 12px */ | |
| box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); | |
| transition: box-shadow 0.2s ease, transform 0.15s ease; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| /* Better hover feedback */ | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.25); | |
| transform: scale(1.05); | |
| } | |
| /* Active state */ | |
| input[type="range"]:active::-webkit-slider-thumb { | |
| box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.3); | |
| } | |
| /* Keyboard accessibility */ | |
| input[type="range"]:focus { | |
| outline: none; | |
| } | |
| input[type="range"]:focus-visible::-webkit-slider-thumb { | |
| box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.35); | |
| } | |
| /* DROPDOWNS & INPUTS */ | |
| select, | |
| input[type="text"] { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.3); | |
| color: var(--clr-text); | |
| font-family: inherit; | |
| font-size: 15px; | |
| padding: 10px 0; | |
| border-radius: 0; | |
| cursor: pointer; | |
| transition: border-color 0.2s; | |
| /* Force custom look */ | |
| -webkit-appearance: none; | |
| appearance: none; | |
| color-scheme: dark; | |
| } | |
| /* Custom Arrow for Dropdowns */ | |
| select { | |
| background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); | |
| background-repeat: no-repeat; | |
| background-position: right 0 top 50%; | |
| background-size: 10px auto; | |
| padding-right: 20px; | |
| /* Force transparency on Mac */ | |
| background-color: transparent ; | |
| -moz-appearance: none; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| } | |
| select:focus, | |
| input:focus { | |
| outline: none; | |
| border-bottom-color: var(--clr-text); | |
| } | |
| select option { | |
| background-color: #0700d6; | |
| color: #ffffff; | |
| padding: 12px 16px; | |
| } | |
| /* Helper text context */ | |
| .helper-text { | |
| font-size: 12px; | |
| opacity: 0.6; | |
| margin-top: 8px; | |
| font-style: italic; | |
| } | |
| /* TOGGLE BUTTONS */ | |
| .toggle-group { | |
| display: flex; | |
| gap: 20px; | |
| margin-top: 20px; | |
| } | |
| .toggle-opt { | |
| cursor: pointer; | |
| font-size: 14px; | |
| opacity: 0.5; | |
| padding-bottom: 4px; | |
| border-bottom: 1px solid transparent; | |
| transition: all 0.2s; | |
| } | |
| .toggle-opt:hover { | |
| opacity: 0.8; | |
| } | |
| .toggle-opt.active { | |
| opacity: 1; | |
| border-bottom-color: var(--clr-text); | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| /* ACTION AREA */ | |
| .action-bar { | |
| margin-top: 60px; | |
| text-align: center; | |
| } | |
| button.primary-btn { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--clr-text); | |
| border: 1px solid rgba(255, 255, 255, 0.4); | |
| border-radius: 6px; | |
| /* Standardized radius */ | |
| padding: 18px 40px; | |
| font-size: 16px; | |
| font-family: inherit; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| letter-spacing: 0.05em; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| button.primary-btn:hover { | |
| background: rgba(255, 255, 255, 0.15); | |
| box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); | |
| border-color: rgba(255, 255, 255, 0.8); | |
| /* Removed text-shadow per requested polish */ | |
| } | |
| button.secondary-btn { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.4); | |
| color: var(--clr-text); | |
| padding: 8px 16px; | |
| font-size: 13px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| font-family: inherit; | |
| flex: 1; | |
| transition: all 0.2s ease; | |
| } | |
| button.secondary-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-color: rgba(255, 255, 255, 0.7); | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: default; | |
| } | |
| /* RESULTS */ | |
| .result-box { | |
| margin-top: 60px; | |
| padding-top: 40px; | |
| border-top: 1px solid var(--clr-divider); | |
| display: none; | |
| animation: fadeUp 0.8s ease forwards; | |
| } | |
| .result-score { | |
| font-size: 48px; | |
| font-weight: 500; | |
| /* Reduced weight */ | |
| margin-bottom: 10px; | |
| letter-spacing: -0.02em; | |
| } | |
| .result-detail { | |
| font-size: 16px; | |
| opacity: 0.8; | |
| line-height: 1.6; | |
| } | |
| @keyframes fadeUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* ======================================== | |
| CUSTOM DROPDOWN STYLES | |
| ======================================== */ | |
| /* Hide native select elements */ | |
| select { | |
| display: none; | |
| } | |
| /* Custom Select Container */ | |
| .custom-select { | |
| position: relative; | |
| width: 100%; | |
| } | |
| /* Trigger (the clickable field) */ | |
| .select-trigger { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.3); | |
| color: var(--clr-text); | |
| font-family: inherit; | |
| font-size: 14px; | |
| /* Reduced from 15px */ | |
| font-weight: 400; | |
| /* Explicit weight */ | |
| padding: 10px 20px 10px 0; | |
| cursor: pointer; | |
| transition: border-color 0.2s; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| user-select: none; | |
| line-height: 1.5; | |
| /* Generous line-height */ | |
| } | |
| .select-trigger:hover { | |
| border-bottom-color: rgba(255, 255, 255, 0.5); | |
| } | |
| .custom-select.open .select-trigger { | |
| border-bottom-color: var(--clr-text); | |
| } | |
| .select-value { | |
| flex: 1; | |
| } | |
| /* Toned down arrow */ | |
| .select-arrow { | |
| font-size: 8px; | |
| /* Reduced by ~20% from 10px */ | |
| transition: transform 0.3s ease-out; | |
| /* Slower, smoother */ | |
| opacity: 0.6; | |
| /* Reduced from 0.8 */ | |
| } | |
| .custom-select.open .select-arrow { | |
| transform: rotate(180deg); | |
| } | |
| /* Options Panel - Calmer blue */ | |
| .select-options { | |
| position: absolute; | |
| top: 100%; | |
| left: 0; | |
| right: 0; | |
| /* Slightly darker than page background for embedded feel */ | |
| background: linear-gradient(to bottom, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.05)), var(--clr-bg); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| /* Softer border */ | |
| border-radius: 4px; | |
| margin-top: 8px; | |
| max-height: 280px; | |
| overflow-y: auto; | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translateY(-10px); | |
| transition: all 0.2s ease; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| } | |
| .custom-select.open .select-options { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateY(0); | |
| } | |
| /* Individual Option - Quieter */ | |
| .select-option { | |
| padding: 12px 16px; | |
| color: var(--clr-text); | |
| cursor: pointer; | |
| transition: background-color 0.15s; | |
| font-size: 14px; | |
| /* Reduced from 15px */ | |
| font-weight: 400; | |
| /* Normal weight */ | |
| line-height: 1.4; | |
| } | |
| /* Calmer hover */ | |
| .select-option:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| /* Reduced from 0.12 */ | |
| } | |
| /* Quieter selected state - soft background only */ | |
| .select-option.selected { | |
| background: rgba(255, 255, 255, 0.06); | |
| /* Very subtle */ | |
| font-weight: 500; | |
| /* Max 500, not 600 */ | |
| } | |
| /* Subtle checkmark */ | |
| .select-option.selected::after { | |
| content: ' ✓'; | |
| float: right; | |
| opacity: 0.6; | |
| /* Reduced from 0.7 */ | |
| font-size: 12px; | |
| /* Smaller */ | |
| } | |
| /* Keyboard Navigation - No border, just soft highlight */ | |
| .select-option.keyboard-focus { | |
| background: rgba(255, 255, 255, 0.10); | |
| /* Removed outline border */ | |
| } | |
| /* Custom Scrollbar for Options */ | |
| .select-options::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .select-options::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .select-options::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.3); | |
| border-radius: 4px; | |
| } | |
| .select-options::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.5); | |
| } | |
| /* Accessibility: Focus State */ | |
| .custom-select:focus-within .select-trigger { | |
| border-bottom-color: var(--clr-text); | |
| outline: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Fraud Detection System</h1> | |
| <div class="subtitle">Predictive analysis of auto claim irregularities</div> | |
| </header> | |
| <!-- CONFIG --> | |
| <div class="config-bar"> | |
| <!-- Model Selection --> | |
| <div class="control-group"> | |
| <label>Prediction Model</label> | |
| <select id="model"> | |
| <option value="xgb" selected>XGBoost — Quick signal</option> | |
| <option value="voting">Voting Ensemble — Best overall signal</option> | |
| <option value="et">Extra Trees — Stable signal</option> | |
| <option value="rf">Random Forest — Clear reasoning</option> | |
| </select> | |
| <!-- Updated Style: Removed font-size 13px and opacity 0.8 for cleaner look, added font-weight:600 to reference model --> | |
| <div class="helper-text" style="line-height: 1.4; margin-top: 8px;"> | |
| Risk drivers are generated using a <span style="font-weight: 600;">ExtraTrees uncalibrated reference | |
| model</span> for consistency | |
| and interpretability. | |
| <br>Prediction scores are produced by the selected model above. | |
| </div> | |
| <!-- Removed modelHint div --> | |
| </div> | |
| <!-- Analysis Mode --> | |
| <div class="control-group"> | |
| <label>Analysis Mode</label> | |
| <select id="scenario"> | |
| <option value="auto_flagger" selected>Auto-Flagger (Operational)</option> | |
| <option value="dashboard">Dashboard Review (Calibrated)</option> | |
| </select> | |
| <div id="scenario_desc" class="helper-text" style="line-height: 1.4; margin-top: 8px;"> | |
| Optimized for detecting potentially fraudulent claims. Designed for automated or semi-automated | |
| flagging where recall is critical. | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 1 --> | |
| <div class="tier-section"> | |
| <div class="tier-header"><span class="tier-title">01 / Incident & Injury</span></div> | |
| <div class="form-grid"> | |
| <div class="control-group"> | |
| <label>Incident Severity</label> | |
| <select id="incident_severity"> | |
| <option value="Major Damage">Major damage</option> | |
| <option value="Minor Damage" selected>Minor damage</option> | |
| <option value="Total Loss">Total loss</option> | |
| <option value="Trivial Damage">Trivial damage</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Collision Type</label> | |
| <select id="collision_type"> | |
| <option value="Rear Collision" selected>Rear collision</option> | |
| <option value="Front Collision">Front collision</option> | |
| <option value="Side Collision">Side collision</option> | |
| <option value="?">Unknown / Not Reported</option> | |
| </select> | |
| </div> | |
| <!-- Incident Time (Moved from Tier 2) --> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Incident Time</label><span class="slider-val" | |
| id="val_hour">12:00</span></div> | |
| <input type="range" id="incident_hour_of_the_day" min="0" max="23" step="1" value="12"> | |
| <div class="helper-text" id="helper_hour">Afternoon</div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Bodily Injuries</label> | |
| <select id="bodily_injuries"> | |
| <option value="0">0</option> | |
| <option value="1" selected>1</option> | |
| <option value="2">2</option> | |
| <option value="3">3+</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 2 --> | |
| <div class="tier-section"> | |
| <div class="tier-header"><span class="tier-title">02 / Claim Amount</span></div> | |
| <div class="control-group" style="margin-bottom: 30px;"> | |
| <div class="slider-header"><label>Total Claim Amount ($)</label><span class="slider-val" | |
| id="val_total_claim">$58,000</span></div> | |
| <div class="helper-text" style="opacity: 0.6; margin-bottom: 10px;">Total amount claimed before internal | |
| allocation.</div> | |
| <input type="range" id="total_claim_amount" min="1000" max="100000" step="1000" value="58000"> | |
| </div> | |
| <!-- BREAKDOWN BLOCK --> | |
| <div | |
| style="margin-top: 30px; background: rgba(255,255,255,0.03); padding: 25px 25px 15px 25px; border-radius: 4px; margin-left: 20px; border-left: 2px solid var(--clr-divider);"> | |
| <div style="font-size: 14px; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 5px;"> | |
| Claim Amount Breakdown</div> | |
| <div style="font-size: 13px; opacity: 0.6; margin-bottom: 25px; font-style: italic;">Estimated | |
| distribution of the total claim amount (percentages are relative and do not need to sum perfectly) | |
| </div> | |
| <div class="form-grid"> | |
| <!-- Injury Portion --> | |
| <div class="control-group"> | |
| <div class="slider-header"> | |
| <label>Injury-Related Portion</label> | |
| <span class="slider-val" id="val_injury_share">10%</span> | |
| </div> | |
| <input type="range" id="injury_share" min="0" max="100" step="5" value="10"> | |
| <div class="helper-text">Medical and bodily injury related costs</div> | |
| </div> | |
| <!-- Property Portion --> | |
| <div class="control-group"> | |
| <div class="slider-header"> | |
| <label>Property Damage Portion</label> | |
| <span class="slider-val" id="val_prop_share">10%</span> | |
| </div> | |
| <input type="range" id="property_share" min="0" max="100" step="5" value="10"> | |
| <div class="helper-text">Vehicle and physical property damage</div> | |
| </div> | |
| </div> | |
| <!-- Remainder --> | |
| <div | |
| style="margin-top: 25px; border-top: 1px solid var(--clr-divider); padding-top: 15px; text-align: right;"> | |
| <label style="opacity: 0.5; font-size: 12px; display: block; margin-bottom: 4px;">Other / | |
| Administrative (Calculated)</label> | |
| <div id="val_remainder" | |
| style="font-size: 16px; opacity: 0.7; font-family: 'Shippori Mincho B1', serif;">80%</div> | |
| <div class="helper-text" style="text-align: right; opacity: 0.4;">Remaining claim components (legal, | |
| admin, other)</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 3 --> | |
| <div class="tier-section"> | |
| <div class="tier-header"><span class="tier-title">03 / Policy & Tenure</span></div> | |
| <div class="form-grid"> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Annual Premium</label><span class="slider-val" | |
| id="val_premium">$1,200</span></div> | |
| <input type="range" id="policy_annual_premium" min="500" max="3000" step="100" value="1200"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Months as Customer</label><span class="slider-val" | |
| id="val_tenure">200</span></div> | |
| <input type="range" id="months_as_customer" min="0" max="500" step="12" value="200"> | |
| </div> | |
| <!-- Policy Age Abstracted --> | |
| <div class="control-group"> | |
| <label>When did the policy start?</label> | |
| <select id="days_since_bind"> | |
| <option value="90">Less than 6 months ago</option> | |
| <option value="270">6–12 months ago</option> | |
| <option value="540">1–2 years ago</option> | |
| <option value="1500" selected>2–5 years ago</option> | |
| <option value="3000">More than 5 years ago</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 4 --> | |
| <div class="tier-section"> | |
| <div class="tier-header"><span class="tier-title">04 / Vehicle Context</span></div> | |
| <div class="form-grid"> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Vehicle Age</label><span class="slider-val" id="val_veh_age">10 | |
| yrs</span></div> | |
| <input type="range" id="vehicle_age" min="0" max="30" step="1" value="10"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Vehicles Involved</label> | |
| <select id="number_of_vehicles_involved"> | |
| <option value="1" selected>1</option> | |
| <option value="2">2</option> | |
| <option value="3">3</option> | |
| <option value="4">4+</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 5 --> | |
| <div class="tier-section"> | |
| <div class="tier-header"><span class="tier-title">05 / Administrative</span></div> | |
| <div class="form-grid"> | |
| <div class="control-group"> | |
| <label>Police Report Filed</label> | |
| <select id="police_report_available"> | |
| <option value="YES">Yes</option> | |
| <option value="NO">No</option> | |
| <option value="?">Unknown</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Authorities Involved</label> | |
| <select id="authorities_contacted"> | |
| <option value="Police" selected>Police</option> | |
| <option value="Fire">Fire department</option> | |
| <option value="Ambulance">Ambulance</option> | |
| <option value="Other">Other</option> | |
| <option value="None">None</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TIER 6 --> | |
| <div class="tier-section" style="border-bottom: none;"> | |
| <div class="tier-header"><span class="tier-title">06 / Edge Variables</span></div> | |
| <div class="form-grid"> | |
| <div class="control-group"> | |
| <label>Umbrella Limit</label> | |
| <select id="umbrella_limit"> | |
| <option value="0" selected>None</option> | |
| <option value="1000000">$1M</option> | |
| <option value="5000000">$5M</option> | |
| <option value="10000000">$10M+</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Insured Age</label><span class="slider-val" id="val_age">38 | |
| yrs</span></div> | |
| <input type="range" id="age" min="18" max="100" step="1" value="38"> | |
| </div> | |
| </div> | |
| <!-- CAPITAL IMPACT --> | |
| <!-- FINANCIAL CAPITAL ACTIVITY --> | |
| <div class="control-group" | |
| style="margin-top: 40px; border-top: 1px solid var(--clr-divider); padding-top: 25px;"> | |
| <div style="font-size: 14px; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 5px;"> | |
| Financial Capital Activity (Optional)</div> | |
| <div style="font-size: 13px; opacity: 0.6; margin-bottom: 25px; font-style: italic;">Summary of observed | |
| capital inflows and outflows around the incident period.</div> | |
| <div class="form-grid"> | |
| <!-- Gains --> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Capital Gains</label><span class="slider-val" | |
| id="val_cap_gain">$0</span></div> | |
| <input type="range" id="capital_gains" min="0" max="100000" step="5000" value="0"> | |
| </div> | |
| <!-- Losses --> | |
| <div class="control-group"> | |
| <div class="slider-header"><label>Capital Losses</label><span class="slider-val" | |
| id="val_cap_loss">$0</span></div> | |
| <input type="range" id="capital_loss" min="0" max="100000" step="5000" value="0"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ACTION --> | |
| <div class="action-bar"> | |
| <button class="primary-btn" id="predictBtn">Analyze Claim</button> | |
| <div id="errorBox" style="color: #ffcccc; margin-top: 20px; display: none; font-size: 14px;"></div> | |
| </div> | |
| <!-- RESULT --> | |
| <div id="resultBox" class="result-box" style="display:none; text-align: left; padding-top: 20px;"> | |
| <!-- 1. PRIMARY RISK SCORE (Vertical, Authoritative) --> | |
| <div class="result-section" style="margin-bottom: 30px;"> | |
| <div id="resScoreVal" style="font-size: 56px; font-weight: 500; line-height: 1; margin-bottom: 10px;"> | |
| --%</div> | |
| <div id="resScoreLabel" | |
| style="font-size: 14px; text-transform: uppercase; letter-spacing: 0.15em; font-weight: 500; opacity: 0.7;"> | |
| --</div> | |
| </div> | |
| <!-- 2. RISK CONTEXT --> | |
| <div class="result-section" style="margin-bottom: 30px;"> | |
| <div | |
| style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.5; margin-bottom: 5px;"> | |
| Risk Context</div> | |
| <div id="resContextText" style="font-size: 15px; font-style: italic; opacity: 0.9;">--</div> | |
| </div> | |
| <!-- 3. DECISION RECOMMENDATION (Card Style) --> | |
| <div class="result-section" | |
| style="margin-bottom: 20px; padding: 20px; border-radius: 6px; background: rgba(255,255,255,0.08);"> | |
| <div | |
| style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.6; margin-bottom: 12px;"> | |
| Recommended Action</div> | |
| <div id="resActionHeader" style="font-size: 16px; font-weight: 500; margin-bottom: 15px;">--</div> | |
| <div style="font-size: 14px; opacity: 0.8; margin-bottom: 5px;">Next steps:</div> | |
| <div id="resActionSteps" style="font-size: 14px; line-height: 1.6; opacity: 0.9; padding-left: 5px;">-- | |
| </div> | |
| <!-- SHAP TRIGGER --> | |
| <div style="display: flex; gap: 10px; margin-top: 20px;"> | |
| <button id="viewDriversBtn" class="secondary-btn shine-hover"> | |
| View risk drivers</button> | |
| <button id="explainWhyBtn" class="secondary-btn shine-hover"> | |
| Explain Why ✨</button> | |
| </div> | |
| </div> | |
| <!-- RISK DRIVERS PANEL (Hidden) --> | |
| <div id="driversPanel" | |
| style="display:none; margin-top: 25px; padding-top: 20px; border-top: 1px solid var(--clr-divider);"> | |
| <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Risk Drivers (Model-Based | |
| Explanation)</div> | |
| <!-- Mandatory Disclosure --> | |
| <div style="font-size: 12px; line-height: 1.5; opacity: 0.6; margin-bottom: 20px;"> | |
| These drivers are generated using an explainable <b>ExtraTrees uncalibrated reference model</b> | |
| trained on the same data. | |
| They indicate patterns associated with risk, not the internal logic of the selected prediction | |
| model. | |
| </div> | |
| <div id="driversList" style="display: block;"></div> | |
| <div style="margin-top: 15px; font-size: 11px; opacity: 0.7; font-style: italic;"> | |
| These factors indicate statistical associations, not proof of fraud. | |
| </div> | |
| </div> | |
| <!-- LLM SUMMARY PANEL (Hidden) --> | |
| <div id="llmSummaryBox" class="result-section" | |
| style="display:none; margin-top: 25px; padding-top: 20px; border-top: 1px solid var(--clr-divider);"> | |
| <div | |
| style="font-size: 16px; font-weight: 500; margin-bottom: 12px; display: flex; align-items: center; gap: 10px;"> | |
| How to read this score <span | |
| style="font-size: 11px; opacity: 0.8; font-weight: 600; border: 1px solid rgba(255,255,255,0.5); padding: 2px 6px; border-radius: 4px; letter-spacing: 0.05em;">AI | |
| GENERATED</span> | |
| </div> | |
| <div id="llmSummaryText" style="font-size: 15px; opacity: 0.9; line-height: 1.6; margin-bottom: 15px;"> | |
| </div> | |
| <ul id="llmBullets" | |
| style="margin-bottom: 15px; padding-left: 20px; opacity: 0.85; font-size: 14px; line-height: 1.5;"> | |
| </ul> | |
| <div id="llmDisclaimer" | |
| style="font-size: 12px; opacity: 0.5; font-style: italic; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px;"> | |
| </div> | |
| </div> | |
| <!-- 4. METADATA (Single Line, Quiet) --> | |
| <div id="metaFooter" | |
| style="border-top: 1px solid var(--clr-divider); margin-top: 20px; padding-top: 15px; font-size: 11px; opacity: 0.4; text-align: center;"> | |
| -- | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ======================================== | |
| // CUSTOM DROPDOWN IMPLEMENTATION | |
| // ======================================== | |
| class CustomDropdown { | |
| constructor(selectElement) { | |
| this.selectEl = selectElement; | |
| this.id = selectElement.id; | |
| this.options = Array.from(selectElement.options).map(opt => ({ | |
| value: opt.value, | |
| text: opt.textContent, | |
| selected: opt.selected | |
| })); | |
| this.currentIndex = this.options.findIndex(opt => opt.selected); | |
| if (this.currentIndex === -1) this.currentIndex = 0; | |
| this.keyboardIndex = this.currentIndex; | |
| this.isOpen = false; | |
| this.render(); | |
| this.bindEvents(); | |
| } | |
| render() { | |
| // Create custom dropdown HTML | |
| const customSelect = document.createElement('div'); | |
| customSelect.className = 'custom-select'; | |
| customSelect.setAttribute('data-select-id', this.id); | |
| customSelect.setAttribute('role', 'combobox'); | |
| customSelect.setAttribute('aria-expanded', 'false'); | |
| customSelect.setAttribute('aria-labelledby', this.id + '-label'); | |
| customSelect.setAttribute('tabindex', '0'); | |
| // Trigger | |
| const trigger = document.createElement('div'); | |
| trigger.className = 'select-trigger'; | |
| trigger.innerHTML = ` | |
| <span class="select-value">${this.options[this.currentIndex].text}</span> | |
| <span class="select-arrow">▼</span> | |
| `; | |
| // Options panel | |
| const optionsPanel = document.createElement('div'); | |
| optionsPanel.className = 'select-options'; | |
| optionsPanel.setAttribute('role', 'listbox'); | |
| this.options.forEach((option, index) => { | |
| const optionEl = document.createElement('div'); | |
| optionEl.className = 'select-option'; | |
| if (index === this.currentIndex) optionEl.classList.add('selected'); | |
| optionEl.setAttribute('data-value', option.value); | |
| optionEl.setAttribute('data-index', index); | |
| optionEl.setAttribute('role', 'option'); | |
| optionEl.setAttribute('aria-selected', index === this.currentIndex); | |
| optionEl.textContent = option.text; | |
| optionsPanel.appendChild(optionEl); | |
| }); | |
| customSelect.appendChild(trigger); | |
| customSelect.appendChild(optionsPanel); | |
| // Replace original select (hide it) | |
| this.selectEl.style.display = 'none'; | |
| this.selectEl.parentNode.insertBefore(customSelect, this.selectEl); | |
| // Store references | |
| this.customSelectEl = customSelect; | |
| this.triggerEl = trigger; | |
| this.optionsPanelEl = optionsPanel; | |
| this.optionEls = Array.from(optionsPanel.querySelectorAll('.select-option')); | |
| } | |
| bindEvents() { | |
| // Click on trigger | |
| this.triggerEl.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.toggle(); | |
| }); | |
| // Click on options | |
| this.optionEls.forEach((optionEl, index) => { | |
| optionEl.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.selectOption(index); | |
| this.close(); | |
| }); | |
| }); | |
| // Keyboard navigation | |
| this.customSelectEl.addEventListener('keydown', (e) => this.handleKeyboard(e)); | |
| // Close on outside click | |
| document.addEventListener('click', (e) => { | |
| if (!this.customSelectEl.contains(e.target)) { | |
| this.close(); | |
| } | |
| }); | |
| } | |
| toggle() { | |
| this.isOpen ? this.close() : this.open(); | |
| } | |
| open() { | |
| this.isOpen = true; | |
| this.customSelectEl.classList.add('open'); | |
| this.customSelectEl.setAttribute('aria-expanded', 'true'); | |
| this.keyboardIndex = this.currentIndex; | |
| this.updateKeyboardFocus(); | |
| } | |
| close() { | |
| this.isOpen = false; | |
| this.customSelectEl.classList.remove('open'); | |
| this.customSelectEl.setAttribute('aria-expanded', 'false'); | |
| this.clearKeyboardFocus(); | |
| } | |
| selectOption(index) { | |
| this.currentIndex = index; | |
| // Update UI | |
| this.optionEls.forEach((el, i) => { | |
| el.classList.toggle('selected', i === index); | |
| el.setAttribute('aria-selected', i === index); | |
| }); | |
| const valueEl = this.triggerEl.querySelector('.select-value'); | |
| valueEl.textContent = this.options[index].text; | |
| // Update original select element (maintains form compatibility) | |
| this.selectEl.value = this.options[index].value; | |
| // Trigger change event on original select (for existing JS) | |
| const event = new Event('change', { bubbles: true }); | |
| this.selectEl.dispatchEvent(event); | |
| } | |
| handleKeyboard(e) { | |
| if (!this.isOpen && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown')) { | |
| e.preventDefault(); | |
| this.open(); | |
| return; | |
| } | |
| if (!this.isOpen) return; | |
| switch (e.key) { | |
| case 'Escape': | |
| e.preventDefault(); | |
| this.close(); | |
| break; | |
| case 'Enter': | |
| case ' ': | |
| e.preventDefault(); | |
| this.selectOption(this.keyboardIndex); | |
| this.close(); | |
| break; | |
| case 'ArrowDown': | |
| e.preventDefault(); | |
| this.keyboardIndex = Math.min(this.keyboardIndex + 1, this.options.length - 1); | |
| this.updateKeyboardFocus(); | |
| this.scrollToKeyboardOption(); | |
| break; | |
| case 'ArrowUp': | |
| e.preventDefault(); | |
| this.keyboardIndex = Math.max(this.keyboardIndex - 1, 0); | |
| this.updateKeyboardFocus(); | |
| this.scrollToKeyboardOption(); | |
| break; | |
| case 'Tab': | |
| this.close(); | |
| break; | |
| } | |
| } | |
| updateKeyboardFocus() { | |
| this.optionEls.forEach((el, i) => { | |
| el.classList.toggle('keyboard-focus', i === this.keyboardIndex); | |
| }); | |
| } | |
| clearKeyboardFocus() { | |
| this.optionEls.forEach(el => el.classList.remove('keyboard-focus')); | |
| } | |
| scrollToKeyboardOption() { | |
| const option = this.optionEls[this.keyboardIndex]; | |
| if (option) { | |
| option.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| } | |
| } | |
| } | |
| // Initialize all dropdowns on page load | |
| function initCustomDropdowns() { | |
| const selects = document.querySelectorAll('select'); | |
| selects.forEach(select => { | |
| if (select.id) { // Only convert selects with IDs | |
| new CustomDropdown(select); | |
| } | |
| }); | |
| } | |
| // Run after DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initCustomDropdowns); | |
| } else { | |
| initCustomDropdowns(); | |
| } | |
| // --- LIVE UPDATES --- | |
| const updateVal = (id, fmt, suffix = '') => { | |
| const el = document.getElementById(id); | |
| const display = document.getElementById('val_' + (id === 'incident_hour_of_the_day' ? 'hour' : | |
| id === 'policy_annual_premium' ? 'premium' : | |
| id === 'total_claim_amount' ? 'total_claim' : | |
| id === 'months_as_customer' ? 'tenure' : | |
| id === 'vehicle_age' ? 'veh_age' : | |
| id === 'injury_share' ? 'injury_share' : | |
| id === 'property_share' ? 'prop_share' : | |
| id === 'capital_gains' ? 'cap_gain' : | |
| id === 'capital_loss' ? 'cap_loss' : id)); | |
| if (!el || !display) return; | |
| el.addEventListener('input', () => { | |
| let val = el.value; | |
| if (fmt === '$') val = '$' + parseInt(val).toLocaleString(); | |
| if (fmt === '%') val = val + '%'; | |
| if (suffix) val = val + ' ' + suffix; | |
| if (id === 'incident_hour_of_the_day') { | |
| val = (val < 10 ? '0' + val : val) + ":00"; | |
| const h = parseInt(el.value); | |
| let period = "Night"; | |
| if (h >= 6 && h < 12) period = "Morning"; | |
| else if (h >= 12 && h < 18) period = "Afternoon"; | |
| else if (h >= 18) period = "Evening"; | |
| document.getElementById('helper_hour').innerText = period; | |
| } | |
| display.innerText = val; | |
| }); | |
| }; | |
| updateVal('total_claim_amount', '$'); | |
| // Attach listeners | |
| ['policy_annual_premium', 'capital_gains'].forEach(id => updateVal(id, '$')); | |
| ['capital_loss'].forEach(id => updateVal(id, '$')); // Negative is fine | |
| // Removed injury/prop/total from here as they have custom logic now | |
| ['months_as_customer'].forEach(id => updateVal(id, '')); | |
| updateVal('vehicle_age', '', 'yrs'); | |
| updateVal('age', '', 'yrs'); | |
| updateVal('incident_hour_of_the_day', ''); | |
| // --- BREAKDOWN MATH --- | |
| function updateBreakdown() { | |
| const total = parseFloat(document.getElementById('total_claim_amount').value); | |
| const injuryPct = parseFloat(document.getElementById('injury_share').value); | |
| const propPct = parseFloat(document.getElementById('property_share').value); | |
| const injuryDol = total * (injuryPct / 100.0); | |
| const propDol = total * (propPct / 100.0); | |
| // Remainder | |
| const remainderPct = 100 - injuryPct - propPct; | |
| const remainderDol = total * (remainderPct / 100.0); | |
| // Update displays | |
| document.getElementById('val_total_claim').innerText = "$" + total.toLocaleString(); | |
| document.getElementById('val_injury_share').innerText = `${injuryPct}% ($${Math.round(injuryDol).toLocaleString()})`; | |
| document.getElementById('val_prop_share').innerText = `${propPct}% ($${Math.round(propDol).toLocaleString()})`; | |
| const remEl = document.getElementById('val_remainder'); | |
| remEl.innerText = `${remainderPct}% ($${Math.round(remainderDol).toLocaleString()})`; | |
| // Visual warning for over-allocation | |
| if (remainderPct < 0) { | |
| remEl.style.color = "#ff9999"; | |
| remEl.innerText += " [OVER-ALLOCATED]"; | |
| } else { | |
| remEl.style.color = "inherit"; | |
| } | |
| } | |
| // Attach breakdown listeners | |
| ['total_claim_amount', 'injury_share', 'property_share'].forEach(id => { | |
| document.getElementById(id).addEventListener('input', updateBreakdown); | |
| }); | |
| // Init breakdown | |
| updateBreakdown(); | |
| // --- SCENARIO DESCRIPTION Logic --- | |
| const scenarioDesc = { | |
| 'auto_flagger': "Optimized for detecting potentially fraudulent claims. Designed for automated or semi-automated flagging where recall is critical.", | |
| 'dashboard': "Provides well-calibrated probabilities for human review and reporting. Recommended when the score is used for explanation or decision support." | |
| }; | |
| document.getElementById('scenario').addEventListener('change', (e) => { | |
| document.getElementById('scenario_desc').innerText = scenarioDesc[e.target.value]; | |
| }); | |
| // Removed Model Hint Logic | |
| // Global state for explanation | |
| let lastExplanation = []; | |
| // BEHAVIORAL FIX: Invalidate outputs on input change | |
| const inputs = document.querySelectorAll('input, select'); | |
| inputs.forEach(el => { | |
| el.addEventListener('input', () => { | |
| // Hide AI panels | |
| document.getElementById('llmSummaryBox').style.display = 'none'; | |
| document.getElementById('driversPanel').style.display = 'none'; | |
| // Hide Score + Result Card ("INVALIDATION" fix) | |
| document.getElementById('resultBox').style.display = 'none'; | |
| }); | |
| }); | |
| // Toggle Drivers | |
| document.getElementById('viewDriversBtn').addEventListener('click', () => { | |
| const panel = document.getElementById('driversPanel'); | |
| if (panel.style.display === 'none') { | |
| panel.style.display = 'block'; | |
| // Render | |
| const list = document.getElementById('driversList'); | |
| list.innerHTML = ''; | |
| if (!lastExplanation || lastExplanation.length === 0) { | |
| list.innerHTML = '<div style="opacity:0.5; font-size:12px;">No risk drivers available for this model/scenario.</div>'; | |
| return; | |
| } | |
| lastExplanation.forEach(item => { | |
| const isUp = item.direction === 'UP'; | |
| const icon = isUp ? '↑' : '↓'; | |
| // Specific Copy Pattern: | |
| // Primary: Feature Name | |
| // Secondary: Arrow + "Increases risk" / "Reduces risk" | |
| // Tertiary: Details | |
| const directionText = isUp ? "Increases risk" : "Reduces risk"; | |
| const row = document.createElement('div'); | |
| row.style.marginBottom = "20px"; | |
| row.style.borderBottom = "1px solid rgba(255,255,255,0.05)"; | |
| row.style.paddingBottom = "10px"; | |
| row.innerHTML = ` | |
| <div style="font-weight: 600; font-size: 15px; margin-bottom: 4px;">${item.feature}</div> | |
| <div style="font-size: 13px; opacity: 0.9; margin-bottom: 2px; color: ${isUp ? '#ffdddd' : '#ddffdd'};"> | |
| ${icon} ${directionText} | |
| </div> | |
| <div style="font-size: 12px; opacity: 0.6; font-style: italic;"> | |
| Associated with ${isUp ? 'higher' : 'lower'} risk patterns | |
| </div> | |
| `; | |
| list.appendChild(row); | |
| }); | |
| // Scroll to panel | |
| setTimeout(() => { | |
| panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 100); | |
| } else { | |
| panel.style.display = 'none'; | |
| } | |
| }); | |
| // Explain Why Logic | |
| document.getElementById('explainWhyBtn').addEventListener('click', async () => { | |
| const btn = document.getElementById('explainWhyBtn'); | |
| const panel = document.getElementById('llmSummaryBox'); | |
| // UI Loading State | |
| btn.innerText = "Analyzing..."; | |
| btn.style.opacity = "0.7"; | |
| btn.disabled = true; | |
| panel.style.display = 'none'; | |
| try { | |
| // Re-gather payload (reuse logic or grab from inputs) | |
| // PRO TIP: Ideally we'd separate gatherPayload() but for now we duplicate efficiently | |
| const payload = { | |
| incident_severity: document.getElementById('incident_severity').value, | |
| injury_share: parseFloat(document.getElementById('injury_share').value) / 100.0, | |
| collision_type: document.getElementById('collision_type').value, | |
| bodily_injuries: parseInt(document.getElementById('bodily_injuries').value), | |
| total_claim_amount: parseFloat(document.getElementById('total_claim_amount').value), | |
| incident_hour_of_the_day: parseInt(document.getElementById('incident_hour_of_the_day').value), | |
| property_share: parseFloat(document.getElementById('property_share').value) / 100.0, | |
| policy_annual_premium: parseFloat(document.getElementById('policy_annual_premium').value), | |
| months_as_customer: parseInt(document.getElementById('months_as_customer').value), | |
| days_since_bind: parseInt(document.getElementById('days_since_bind').value), | |
| vehicle_age: parseInt(document.getElementById('vehicle_age').value), | |
| number_of_vehicles_involved: parseInt(document.getElementById('number_of_vehicles_involved').value), | |
| police_report_available: document.getElementById('police_report_available').value, | |
| authorities_contacted: document.getElementById('authorities_contacted').value, | |
| umbrella_limit: parseInt(document.getElementById('umbrella_limit').value), | |
| "capital-gains": parseFloat(document.getElementById('capital_gains').value), | |
| "capital-loss": -1 * parseFloat(document.getElementById('capital_loss').value), | |
| age: parseInt(document.getElementById('age').value) | |
| }; | |
| const model = document.getElementById('model').value; | |
| const scenario = document.getElementById('scenario').value; | |
| // Request with llm_explain=true | |
| const response = await fetch(`/predict?model=${model}&scenario=${scenario}&explain=true&llm_explain=true`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) throw new Error(await response.text()); | |
| const data = await response.json(); | |
| // Render LLM Result | |
| const llm = data.llm_explanation; | |
| panel.style.display = 'block'; | |
| if (llm && !llm.error) { | |
| // Summary (+ Guidance if available) | |
| let summaryHtml = llm.summary || "Summary unavailable."; | |
| if (llm.guidance) { | |
| summaryHtml += `<br><br><em style="opacity: 0.9; border-left: 2px solid #666; padding-left: 8px; display:block;">Guidance: ${llm.guidance}</em>`; | |
| } | |
| document.getElementById('llmSummaryText').innerHTML = summaryHtml; | |
| const bulletsUl = document.getElementById('llmBullets'); | |
| bulletsUl.innerHTML = ''; | |
| // NEW SCHEMA: Drivers list | |
| if (llm.drivers && Array.isArray(llm.drivers)) { | |
| llm.drivers.forEach(d => { | |
| const li = document.createElement('li'); | |
| li.style.marginBottom = "8px"; | |
| li.style.lineHeight = "1.4"; | |
| // Color/Arrow for effect | |
| let effectColor = "#ccc"; | |
| let arrow = "•"; | |
| if (d.effect && d.effect.toLowerCase().includes("up")) { arrow = "↑"; effectColor = "#ff9999"; } | |
| if (d.effect && d.effect.toLowerCase().includes("down")) { arrow = "↓"; effectColor = "#99ff99"; } | |
| li.innerHTML = `<span style="color:${effectColor}; font-weight:bold; margin-right:4px;">${arrow}</span> <strong>${d.name}</strong>: ${d.explanation}`; | |
| bulletsUl.appendChild(li); | |
| }); | |
| } | |
| // FALLBACK: Old bullets list | |
| else if (llm.bullets && Array.isArray(llm.bullets)) { | |
| llm.bullets.forEach(b => { | |
| const li = document.createElement('li'); | |
| li.style.marginBottom = "6px"; | |
| li.innerText = b; | |
| bulletsUl.appendChild(li); | |
| }); | |
| } | |
| document.getElementById('llmDisclaimer').innerText = llm.disclaimer || "Disclaimer: These are statistical patterns, not proof."; | |
| } else { | |
| const errorMsg = (llm && llm.error) ? llm.error : "AI explanation unavailable at this time."; | |
| document.getElementById('llmSummaryText').innerHTML = `<span style="color: #ff9999;">⚠ ${errorMsg}</span>`; | |
| document.getElementById('llmBullets').innerHTML = ''; | |
| document.getElementById('llmDisclaimer').innerText = "Please rely on the Risk Drivers list below."; | |
| } | |
| // Scroll to explain box | |
| setTimeout(() => { | |
| panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 100); | |
| } catch (e) { | |
| console.error(e); | |
| alert("Explanation failed: " + e.message); | |
| } finally { | |
| btn.innerText = "Explain Why ✨"; | |
| btn.style.opacity = "1.0"; | |
| btn.disabled = false; | |
| } | |
| }); | |
| // --- CLEAR RESULTS ON CHANGE --- | |
| const clearResults = () => { | |
| const resBox = document.getElementById('resultBox'); | |
| const errBox = document.getElementById('errorBox'); | |
| const driversPanel = document.getElementById('driversPanel'); | |
| const llmSummaryBox = document.getElementById('llmSummaryBox'); | |
| if (resBox && resBox.style.display !== 'none') { | |
| resBox.style.display = 'none'; | |
| errBox.style.display = 'none'; | |
| driversPanel.style.display = 'none'; | |
| llmSummaryBox.style.display = 'none'; | |
| } | |
| }; | |
| // Attach to all inputs | |
| document.querySelectorAll('input, select').forEach(el => { | |
| el.addEventListener('change', clearResults); | |
| el.addEventListener('input', clearResults); | |
| }); | |
| // --- SUBMISSION --- | |
| document.getElementById('predictBtn').addEventListener('click', async () => { | |
| const btn = document.getElementById('predictBtn'); | |
| const resBox = document.getElementById('resultBox'); | |
| const errBox = document.getElementById('errorBox'); | |
| const driversPanel = document.getElementById('driversPanel'); | |
| // Reset | |
| btn.disabled = true; | |
| btn.style.opacity = "0.5"; | |
| btn.innerText = "Processing..."; | |
| resBox.style.display = 'none'; | |
| errBox.style.display = 'none'; | |
| driversPanel.style.display = 'none'; // Auto hide on new predict | |
| document.getElementById('llmSummaryBox').style.display = 'none'; // Auto hide LLM | |
| try { | |
| // Construct Payload | |
| const payload = { | |
| incident_severity: document.getElementById('incident_severity').value, | |
| injury_share: parseFloat(document.getElementById('injury_share').value) / 100.0, | |
| collision_type: document.getElementById('collision_type').value, | |
| bodily_injuries: parseInt(document.getElementById('bodily_injuries').value), | |
| total_claim_amount: parseFloat(document.getElementById('total_claim_amount').value), | |
| incident_hour_of_the_day: parseInt(document.getElementById('incident_hour_of_the_day').value), | |
| property_share: parseFloat(document.getElementById('property_share').value) / 100.0, | |
| policy_annual_premium: parseFloat(document.getElementById('policy_annual_premium').value), | |
| months_as_customer: parseInt(document.getElementById('months_as_customer').value), | |
| days_since_bind: parseInt(document.getElementById('days_since_bind').value), | |
| vehicle_age: parseInt(document.getElementById('vehicle_age').value), | |
| number_of_vehicles_involved: parseInt(document.getElementById('number_of_vehicles_involved').value), | |
| police_report_available: document.getElementById('police_report_available').value, | |
| authorities_contacted: document.getElementById('authorities_contacted').value, | |
| umbrella_limit: parseInt(document.getElementById('umbrella_limit').value), | |
| "capital-gains": parseFloat(document.getElementById('capital_gains').value), | |
| "capital-loss": -1 * parseFloat(document.getElementById('capital_loss').value), | |
| age: parseInt(document.getElementById('age').value) | |
| }; | |
| const model = document.getElementById('model').value; | |
| const scenario = document.getElementById('scenario').value; | |
| // Request explanation by default now | |
| const response = await fetch(`/predict?model=${model}&scenario=${scenario}&explain=true`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) throw new Error(await response.text()); | |
| const data = await response.json(); | |
| // --- RENDER RESULT --- | |
| const prob = data.probability; | |
| const pct = (prob * 100).toFixed(1) + "%"; | |
| // 1. Label Mapping (ALL CAPS) | |
| let label = "LOW RISK"; | |
| if (prob >= 0.3) label = "MODERATE RISK"; | |
| if (prob >= 0.5) label = "HIGH RISK"; | |
| if (prob >= 0.7) label = "VERY HIGH RISK"; | |
| document.getElementById('resScoreVal').innerText = pct; | |
| document.getElementById('resScoreLabel').innerText = label; | |
| // 2. Risk Context | |
| let context = "This claim falls within the typical risk range compared to similar claims."; | |
| if (prob >= 0.6) context = "This claim falls within the elevated risk range compared to similar claims."; | |
| document.getElementById('resContextText').innerText = context; | |
| // 3. Recommendation Card | |
| let header = ""; | |
| let steps = ""; | |
| const isHigh = prob >= 0.5; // operational threshold | |
| if (scenario === 'auto_flagger') { | |
| if (isHigh) { | |
| header = "⚠ Claim exceeds operational threshold"; | |
| steps = "• Automatically flag for further investigation<br>• Route to fraud review queue<br>• Do not approve without manual verification"; | |
| } else { | |
| header = "✓ No immediate fraud action required"; | |
| steps = "• Continue standard processing<br>• Monitor as part of routine checks"; | |
| } | |
| } else { // dashboard | |
| if (isHigh) { | |
| header = "⚠ Elevated risk factors detected"; | |
| steps = "• Review supporting documentation<br>• Verify claim details and timelines<br>• Use as decision support input"; | |
| } else { | |
| header = "✓ Standard risk profile"; | |
| steps = "• Proceed with standard review<br>• Verify basic documentation"; | |
| } | |
| } | |
| document.getElementById('resActionHeader').innerText = header; | |
| document.getElementById('resActionSteps').innerHTML = steps; | |
| // 4. Metadata Footer | |
| const txtModel = model === 'voting' ? "Voting Ensemble" : model.toUpperCase(); | |
| const calibStatus = scenario === 'dashboard' ? "Enabled" : "Disabled"; | |
| const modeStatus = scenario === 'auto_flagger' ? "Auto-Flagger" : "Dashboard Review"; | |
| const ver = data.app_version ? `v${data.app_version}` : "v1.0?"; | |
| document.getElementById('metaFooter').innerText = `Model: ${txtModel} · Calibration: ${calibStatus} · Mode: ${modeStatus} · ${ver}`; | |
| // 5. Store SHAP | |
| lastExplanation = data.explanation || []; | |
| resBox.style.display = 'block'; | |
| // Scroll to Results | |
| setTimeout(() => { | |
| resBox.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 100); | |
| } catch (e) { | |
| errBox.innerText = "System Error: " + e.message; | |
| errBox.style.display = 'block'; | |
| } finally { | |
| btn.disabled = false; | |
| btn.style.opacity = "1"; | |
| btn.innerText = "Analyze Claim"; | |
| } | |
| }); | |
| // Initial setup of function exposure | |
| // Removed setCapMode exposure as it is deleted | |
| </script> | |
| </body> | |
| </html> |