Nausad's picture
Update templates/index.html
7fdce09 verified
<!DOCTYPE html>
<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>