fraud-detector / index.html
MyNameIsTatiBond's picture
Enhance slider usability and add auto-clear results logic
5c9a0e7
<!DOCTYPE html>
<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) !important;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.8) !important;
}
.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 !important;
-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>