Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FraudGuard β Transaction Risk Analyzer</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0b0f; | |
| --surface: #111318; | |
| --surface2: #181b23; | |
| --border: #252830; | |
| --text: #e8eaf0; | |
| --muted: #6b7280; | |
| --accent: #00e5a0; | |
| --accent2: #00b8ff; | |
| --danger: #ff4566; | |
| --warn: #ffb347; | |
| --safe: #00e5a0; | |
| --mono: 'Space Mono', monospace; | |
| --sans: 'Syne', sans-serif; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0,229,160,0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,229,160,0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .container { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 40px 24px 80px; | |
| } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| margin-bottom: 48px; | |
| } | |
| .logo-mark { | |
| width: 48px; height: 48px; | |
| border: 2px solid var(--accent); | |
| border-radius: 12px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: var(--mono); | |
| font-size: 20px; | |
| color: var(--accent); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .logo-mark::after { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(135deg, rgba(0,229,160,0.15), transparent); | |
| } | |
| .header-text h1 { | |
| font-size: 26px; | |
| font-weight: 800; | |
| letter-spacing: -0.5px; | |
| } | |
| .header-text h1 span { color: var(--accent); } | |
| .header-text p { | |
| font-size: 13px; | |
| color: var(--muted); | |
| font-family: var(--mono); | |
| margin-top: 2px; | |
| } | |
| .badge { | |
| margin-left: auto; | |
| background: rgba(0,229,160,0.1); | |
| border: 1px solid rgba(0,229,160,0.3); | |
| color: var(--accent); | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| letter-spacing: 1px; | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 1fr 380px; | |
| gap: 24px; | |
| align-items: start; | |
| } | |
| @media (max-width: 820px) { | |
| .layout { grid-template-columns: 1fr; } | |
| } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 28px; | |
| } | |
| .card-title { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| letter-spacing: 2px; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| margin-bottom: 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .card-title::before { | |
| content: ''; | |
| width: 16px; height: 2px; | |
| background: var(--accent); | |
| display: inline-block; | |
| } | |
| .form-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| .form-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .form-group.full { grid-column: 1 / -1; } | |
| label { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| } | |
| label .hint { | |
| font-size: 10px; | |
| color: #3d4150; | |
| font-style: normal; | |
| margin-left: 4px; | |
| } | |
| input, select { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: 13px; | |
| padding: 10px 12px; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| outline: none; | |
| width: 100%; | |
| } | |
| input:focus, select:focus { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px rgba(0,229,160,0.1); | |
| } | |
| select option { background: var(--surface2); } | |
| .error-calc { | |
| background: rgba(0,184,255,0.06); | |
| border: 1px solid rgba(0,184,255,0.2); | |
| border-radius: 8px; | |
| padding: 10px 14px; | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--accent2); | |
| grid-column: 1 / -1; | |
| line-height: 1.6; | |
| } | |
| .error-calc strong { color: var(--text); display: block; margin-bottom: 3px; } | |
| .computed-row { | |
| grid-column: 1 / -1; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| .computed-field { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .computed-field input { | |
| opacity: 0.55; | |
| cursor: not-allowed; | |
| } | |
| .btn-analyze { | |
| width: 100%; | |
| background: var(--accent); | |
| color: #000; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 14px; | |
| font-family: var(--sans); | |
| font-size: 15px; | |
| font-weight: 800; | |
| letter-spacing: 0.5px; | |
| cursor: pointer; | |
| margin-top: 20px; | |
| transition: opacity 0.2s, transform 0.1s, box-shadow 0.2s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn-analyze:hover { | |
| opacity: 0.92; | |
| box-shadow: 0 0 24px rgba(0,229,160,0.4); | |
| } | |
| .btn-analyze:active { transform: scale(0.99); } | |
| .btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } | |
| .result-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .meter-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 28px 28px 24px; | |
| text-align: center; | |
| transition: border-color 0.4s; | |
| } | |
| .meter-wrap { | |
| position: relative; | |
| width: 200px; | |
| height: 110px; | |
| margin: 0 auto 16px; | |
| } | |
| .meter-svg { width: 200px; height: 110px; } | |
| .meter-value { | |
| position: absolute; | |
| bottom: 0; left: 0; right: 0; | |
| font-family: var(--mono); | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: var(--text); | |
| text-align: center; | |
| line-height: 1; | |
| transition: color 0.4s; | |
| } | |
| .meter-label { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| margin-top: 4px; | |
| } | |
| .verdict { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 20px; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| font-size: 15px; | |
| letter-spacing: 0.5px; | |
| margin-top: 12px; | |
| transition: all 0.4s; | |
| } | |
| .verdict.safe { background: rgba(0,229,160,0.12); color: var(--safe); border: 1px solid rgba(0,229,160,0.3); } | |
| .verdict.danger { background: rgba(255,69,102,0.12); color: var(--danger); border: 1px solid rgba(255,69,102,0.3); } | |
| .verdict.warn { background: rgba(255,179,71,0.12); color: var(--warn); border: 1px solid rgba(255,179,71,0.3); } | |
| .verdict.idle { background: rgba(107,114,128,0.1); color: var(--muted); border: 1px solid var(--border); } | |
| .conf-bars { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; } | |
| .bar-row { | |
| display: grid; | |
| grid-template-columns: 80px 1fr 48px; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 12px; | |
| font-family: var(--mono); | |
| } | |
| .bar-label { color: var(--muted); } | |
| .bar-track { | |
| height: 6px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .bar-fill { | |
| height: 100%; | |
| border-radius: 3px; | |
| transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| width: 0%; | |
| } | |
| .bar-fill.legit { background: var(--safe); } | |
| .bar-fill.fraud { background: var(--danger); } | |
| .bar-pct { color: var(--text); text-align: right; } | |
| .presets-card .card-title { margin-bottom: 16px; } | |
| .preset-list { display: flex; flex-direction: column; gap: 8px; } | |
| .preset-btn { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 10px 14px; | |
| cursor: pointer; | |
| text-align: left; | |
| transition: border-color 0.2s, background 0.2s; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .preset-btn:hover { | |
| border-color: var(--accent); | |
| background: rgba(0,229,160,0.04); | |
| } | |
| .preset-name { | |
| font-family: var(--sans); | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text); | |
| } | |
| .preset-desc { | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| color: var(--muted); | |
| margin-top: 2px; | |
| } | |
| .preset-tag { | |
| font-size: 10px; | |
| font-family: var(--mono); | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| letter-spacing: 0.5px; | |
| flex-shrink: 0; | |
| } | |
| .preset-tag.fraud { background: rgba(255,69,102,0.15); color: var(--danger); } | |
| .preset-tag.legit { background: rgba(0,229,160,0.12); color: var(--safe); } | |
| .breakdown-card { display: none; } | |
| .breakdown-card.show { display: block; } | |
| .feature-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| font-size: 11px; | |
| font-family: var(--mono); | |
| } | |
| .feature-item { | |
| background: var(--surface2); | |
| border-radius: 6px; | |
| padding: 8px 10px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .feature-key { color: var(--muted); } | |
| .feature-val { color: var(--text); font-weight: 700; } | |
| @keyframes pulse-danger { | |
| 0%, 100% { box-shadow: 0 0 0 0 rgba(255,69,102,0.4); } | |
| 50% { box-shadow: 0 0 0 8px rgba(255,69,102,0); } | |
| } | |
| .meter-card.fraud-alert { | |
| animation: pulse-danger 1.5s ease-in-out 3; | |
| border-color: rgba(255,69,102,0.4); | |
| } | |
| @keyframes scan { | |
| from { transform: translateY(-100%); } | |
| to { transform: translateY(100%); } | |
| } | |
| .scanning::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, var(--accent), transparent); | |
| animation: scan 0.4s ease-in-out; | |
| } | |
| .error-msg { | |
| display: none; | |
| background: rgba(255,69,102,0.08); | |
| border: 1px solid rgba(255,69,102,0.3); | |
| border-radius: 8px; | |
| padding: 10px 14px; | |
| font-size: 12px; | |
| font-family: var(--mono); | |
| color: var(--danger); | |
| margin-top: 12px; | |
| grid-column: 1/-1; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 14px; height: 14px; | |
| border: 2px solid rgba(0,0,0,0.3); | |
| border-top-color: #000; | |
| border-radius: 50%; | |
| animation: spin 0.6s linear infinite; | |
| vertical-align: middle; | |
| margin-right: 6px; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="logo-mark">β‘</div> | |
| <div class="header-text"> | |
| <h1>Fraud<span>Guard</span></h1> | |
| <p>Random Forest Β· PaySim Dataset Β· Flask Backend<br> | |
| This model will tell you the payment you're doing is Fraud or not.<br></p> | |
| </div> | |
| <div class="badge">LIVE MODEL</div> | |
| </header> | |
| <div class="layout"> | |
| <!-- LEFT: Input Form --> | |
| <div> | |
| <div class="card"> | |
| <div class="card-title">Transaction Details</div> | |
| <div class="form-grid"> | |
| <div class="form-group full"> | |
| <label>Transaction Type</label> | |
| <select id="type"> | |
| <option value="CASH_IN">CASH_IN β Deposit into account</option> | |
| <option value="CASH_OUT" selected>CASH_OUT β Withdraw cash</option> | |
| <option value="DEBIT">DEBIT β Direct debit payment</option> | |
| <option value="PAYMENT">PAYMENT β Merchant payment</option> | |
| <option value="TRANSFER">TRANSFER β Transfer to another account</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Step <span class="hint">(hour of sim)</span></label> | |
| <input type="number" id="step" value="1" min="1" max="744"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Amount <span class="hint">($)</span></label> | |
| <input type="number" id="amount" value="200000" min="0" step="0.01"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Sender Old Balance</label> | |
| <input type="number" id="oldbalanceOrg" value="200000" min="0" step="0.01"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Sender New Balance</label> | |
| <input type="number" id="newbalanceOrig" value="0" min="0" step="0.01"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Recipient Old Balance</label> | |
| <input type="number" id="oldbalanceDest" value="100000" min="0" step="0.01"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Recipient New Balance</label> | |
| <input type="number" id="newbalanceDest" value="300000" min="0" step="0.01"> | |
| </div> | |
| <div class="error-calc"> | |
| <strong>β Error Balance Fields β auto-computed by server</strong> | |
| errorBalanceOrig = oldBalanceOrg β amount β newBalanceOrig<br> | |
| errorBalanceDest = newBalanceDest β oldBalanceDest β amount | |
| </div> | |
| <div class="computed-row"> | |
| <div class="computed-field form-group"> | |
| <label>errorBalanceOrig</label> | |
| <input type="number" id="errorBalanceOrig" value="β" readonly> | |
| </div> | |
| <div class="computed-field form-group"> | |
| <label>errorBalanceDest</label> | |
| <input type="number" id="errorBalanceDest" value="β" readonly> | |
| </div> | |
| </div> | |
| <div class="error-msg" id="errorMsg"></div> | |
| </div> | |
| <button class="btn-analyze" onclick="analyze()" id="analyzeBtn"> | |
| β‘ Analyze Transaction | |
| </button> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Results --> | |
| <div class="result-panel"> | |
| <!-- Risk Meter --> | |
| <div class="meter-card" id="meterCard"> | |
| <div class="card-title" style="justify-content:center">Risk Score</div> | |
| <div class="meter-wrap"> | |
| <svg class="meter-svg" viewBox="0 0 200 110" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke="#252830" stroke-width="12" stroke-linecap="round"/> | |
| <path d="M 20 100 A 80 80 0 0 1 100 20" fill="none" stroke="rgba(0,229,160,0.15)" stroke-width="12" stroke-linecap="round"/> | |
| <path d="M 100 20 A 80 80 0 0 1 180 100" fill="none" stroke="rgba(255,69,102,0.15)" stroke-width="12" stroke-linecap="round"/> | |
| <path id="meterArc" d="M 20 100 A 80 80 0 0 1 20 100" fill="none" stroke="#6b7280" stroke-width="12" stroke-linecap="round" | |
| style="transition: all 0.6s cubic-bezier(0.4,0,0.2,1)"/> | |
| <line id="meterNeedle" x1="100" y1="100" x2="20" y2="100" | |
| stroke="#e8eaf0" stroke-width="2" stroke-linecap="round" | |
| style="transform-origin: 100px 100px; transition: transform 0.6s cubic-bezier(0.4,0,0.2,1); transform: rotate(0deg)"/> | |
| <circle cx="100" cy="100" r="5" fill="#e8eaf0"/> | |
| </svg> | |
| <div class="meter-value" id="meterValue">β</div> | |
| </div> | |
| <div class="meter-label">FRAUD PROBABILITY</div> | |
| <div id="verdictWrap" style="margin-top:12px"> | |
| <div class="verdict idle">⬑ Awaiting analysis</div> | |
| </div> | |
| <div class="conf-bars"> | |
| <div class="bar-row"> | |
| <span class="bar-label">Legitimate</span> | |
| <div class="bar-track"><div class="bar-fill legit" id="barLegit"></div></div> | |
| <span class="bar-pct" id="pctLegit">β</span> | |
| </div> | |
| <div class="bar-row"> | |
| <span class="bar-label">Fraudulent</span> | |
| <div class="bar-track"><div class="bar-fill fraud" id="barFraud"></div></div> | |
| <span class="bar-pct" id="pctFraud">β</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Feature Snapshot --> | |
| <div class="card breakdown-card" id="breakdownCard"> | |
| <div class="card-title">Input Summary</div> | |
| <div class="feature-grid" id="featureGrid"></div> | |
| </div> | |
| <!-- Preset Examples --> | |
| <div class="card presets-card"> | |
| <div class="card-title">Example Transactions</div> | |
| <div class="preset-list"> | |
| <div class="preset-btn" onclick="loadPreset('fraud_cashout')"> | |
| <div> | |
| <div class="preset-name">Account Drain (CASH_OUT)</div> | |
| <div class="preset-desc">Full balance withdrawn, large amount</div> | |
| </div> | |
| <span class="preset-tag fraud">FRAUD</span> | |
| </div> | |
| <div class="preset-btn" onclick="loadPreset('fraud_transfer')"> | |
| <div> | |
| <div class="preset-name">Ghost Transfer</div> | |
| <div class="preset-desc">Transfer zeroing originator balance</div> | |
| </div> | |
| <span class="preset-tag fraud">FRAUD</span> | |
| </div> | |
| <div class="preset-btn" onclick="loadPreset('legit_payment')"> | |
| <div> | |
| <div class="preset-name">Routine Payment</div> | |
| <div class="preset-desc">Small merchant payment, normal balances</div> | |
| </div> | |
| <span class="preset-tag legit">LEGIT</span> | |
| </div> | |
| <div class="preset-btn" onclick="loadPreset('legit_cashin')"> | |
| <div> | |
| <div class="preset-name">Salary Deposit</div> | |
| <div class="preset-desc">CASH_IN, balance increases correctly</div> | |
| </div> | |
| <span class="preset-tag legit">LEGIT</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // βββ Meter rendering βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setMeter(prob) { | |
| const angle = prob * 180; | |
| const needleAngle = -90 + angle; | |
| document.getElementById('meterNeedle').style.transform = `rotate(${needleAngle}deg)`; | |
| const cx = 100, cy = 100, r = 80; | |
| const startAngle = Math.PI; | |
| const endAngle = Math.PI - (prob * Math.PI); | |
| const x1 = cx + r * Math.cos(startAngle); | |
| const y1 = cy + r * Math.sin(startAngle); | |
| const x2 = cx + r * Math.cos(endAngle); | |
| const y2 = cy + r * Math.sin(endAngle); | |
| let color; | |
| if (prob < 0.3) color = '#00e5a0'; | |
| else if (prob < 0.6) color = '#ffb347'; | |
| else color = '#ff4566'; | |
| const arcEl = document.getElementById('meterArc'); | |
| if (prob === 0) { | |
| arcEl.setAttribute('d', `M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x1} ${y1}`); | |
| } else { | |
| const la = angle > 180 ? 1 : 0; | |
| arcEl.setAttribute('d', `M ${x1} ${y1} A ${r} ${r} 0 ${la} 1 ${x2} ${y2}`); | |
| } | |
| arcEl.setAttribute('stroke', color); | |
| document.getElementById('meterValue').textContent = (prob * 100).toFixed(1) + '%'; | |
| document.getElementById('meterValue').style.color = color; | |
| } | |
| // βββ Analyze via Flask /predict βββββββββββββββββββββββββββββββββββββββββββββ | |
| async function analyze() { | |
| const btn = document.getElementById('analyzeBtn'); | |
| const errDiv = document.getElementById('errorMsg'); | |
| errDiv.style.display = 'none'; | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spinner"></span>Analyzingβ¦'; | |
| btn.classList.add('scanning'); | |
| setTimeout(() => btn.classList.remove('scanning'), 500); | |
| const payload = { | |
| step: document.getElementById('step').value, | |
| type: document.getElementById('type').value, | |
| amount: document.getElementById('amount').value, | |
| oldbalanceOrg: document.getElementById('oldbalanceOrg').value, | |
| newbalanceOrig: document.getElementById('newbalanceOrig').value, | |
| oldbalanceDest: document.getElementById('oldbalanceDest').value, | |
| newbalanceDest: document.getElementById('newbalanceDest').value, | |
| }; | |
| try { | |
| const res = await fetch('/predict', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| if (data.error) throw new Error(data.error); | |
| const fraudProb = data.fraud_prob / 100; | |
| const legitProb = data.legit_prob / 100; | |
| // Update computed error fields | |
| document.getElementById('errorBalanceOrig').value = data.error_balance_orig; | |
| document.getElementById('errorBalanceDest').value = data.error_balance_dest; | |
| // Meter | |
| setMeter(fraudProb); | |
| // Bars | |
| document.getElementById('barLegit').style.width = data.legit_prob + '%'; | |
| document.getElementById('barFraud').style.width = data.fraud_prob + '%'; | |
| document.getElementById('pctLegit').textContent = data.legit_prob.toFixed(1) + '%'; | |
| document.getElementById('pctFraud').textContent = data.fraud_prob.toFixed(1) + '%'; | |
| // Verdict | |
| const meterCard = document.getElementById('meterCard'); | |
| meterCard.classList.remove('fraud-alert'); | |
| let verdictHtml; | |
| if (fraudProb >= 0.7) { | |
| verdictHtml = `<div class="verdict danger">β HIGH FRAUD RISK</div>`; | |
| setTimeout(() => meterCard.classList.add('fraud-alert'), 100); | |
| } else if (fraudProb >= 0.3) { | |
| verdictHtml = `<div class="verdict warn">β SUSPICIOUS β Review</div>`; | |
| } else { | |
| verdictHtml = `<div class="verdict safe">β LIKELY LEGITIMATE</div>`; | |
| } | |
| document.getElementById('verdictWrap').innerHTML = verdictHtml; | |
| // Feature snapshot | |
| const snap = [ | |
| ['type', payload.type], | |
| ['step', payload.step], | |
| ['amount', (+payload.amount).toLocaleString()], | |
| ['senderOld', (+payload.oldbalanceOrg).toLocaleString()], | |
| ['senderNew', (+payload.newbalanceOrig).toLocaleString()], | |
| ['recipOld', (+payload.oldbalanceDest).toLocaleString()], | |
| ['recipNew', (+payload.newbalanceDest).toLocaleString()], | |
| ['errOrig', data.error_balance_orig], | |
| ['errDest', data.error_balance_dest], | |
| ]; | |
| document.getElementById('featureGrid').innerHTML = snap.map(([k, v]) => | |
| `<div class="feature-item"> | |
| <span class="feature-key">${k}</span> | |
| <span class="feature-val">${v}</span> | |
| </div>` | |
| ).join(''); | |
| document.getElementById('breakdownCard').classList.add('show'); | |
| } catch (err) { | |
| errDiv.textContent = 'β ' + err.message; | |
| errDiv.style.display = 'block'; | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = 'β‘ Analyze Transaction'; | |
| } | |
| } | |
| // βββ Presets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const PRESETS = { | |
| fraud_cashout: { step:100, type:'CASH_OUT', amount:200000, oldbalanceOrg:200000, newbalanceOrig:0, oldbalanceDest:100000, newbalanceDest:300000 }, | |
| fraud_transfer: { step:1, type:'TRANSFER', amount:9839.64, oldbalanceOrg:170136, newbalanceOrig:0, oldbalanceDest:0, newbalanceDest:9839.64 }, | |
| legit_payment: { step:50, type:'PAYMENT', amount:500, oldbalanceOrg:10000, newbalanceOrig:9500, oldbalanceDest:0, newbalanceDest:0 }, | |
| legit_cashin: { step:30, type:'CASH_IN', amount:5000, oldbalanceOrg:1000, newbalanceOrig:6000, oldbalanceDest:80000, newbalanceDest:75000 }, | |
| }; | |
| function loadPreset(key) { | |
| const p = PRESETS[key]; | |
| document.getElementById('type').value = p.type; | |
| document.getElementById('step').value = p.step; | |
| document.getElementById('amount').value = p.amount; | |
| document.getElementById('oldbalanceOrg').value = p.oldbalanceOrg; | |
| document.getElementById('newbalanceOrig').value = p.newbalanceOrig; | |
| document.getElementById('oldbalanceDest').value = p.oldbalanceDest; | |
| document.getElementById('newbalanceDest').value = p.newbalanceDest; | |
| analyze(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |