Nausad commited on
Commit
d6e197c
Β·
verified Β·
1 Parent(s): 909a91d

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +65 -0
  2. model.pkl +3 -0
  3. preprocessor.pkl +3 -0
  4. requirements.txt +0 -0
  5. templates/index.html +789 -0
app.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, render_template
2
+ import joblib
3
+ import pandas as pd
4
+ import warnings
5
+ warnings.filterwarnings('ignore')
6
+
7
+ # Compatibility patch for sklearn 1.6.1 pickles on newer sklearn
8
+ from sklearn.compose import _column_transformer
9
+ class _RemainderColsList(list):
10
+ def __init__(self, columns, future_dtype=None):
11
+ super().__init__(columns)
12
+ self.future_dtype = future_dtype
13
+ _column_transformer._RemainderColsList = _RemainderColsList
14
+
15
+ app = Flask(__name__)
16
+
17
+ preprocessor = joblib.load('preprocessor1.pkl')
18
+ model = joblib.load('DecisionTreeModel.pkl')
19
+
20
+ TRANSACTION_TYPES = ['CASH_IN', 'CASH_OUT', 'DEBIT', 'PAYMENT', 'TRANSFER']
21
+
22
+ @app.route('/')
23
+ def index():
24
+ return render_template('index.html')
25
+
26
+ @app.route('/predict', methods=['POST'])
27
+ def predict():
28
+ try:
29
+ d = request.get_json()
30
+
31
+ old_org = float(d['oldbalanceOrg'])
32
+ new_org = float(d['newbalanceOrig'])
33
+ old_dest = float(d['oldbalanceDest'])
34
+ new_dest = float(d['newbalanceDest'])
35
+ amount = float(d['amount'])
36
+
37
+ err_orig = old_org - amount - new_org
38
+ err_dest = new_dest - old_dest - amount
39
+
40
+ df = pd.DataFrame([{
41
+ 'step': float(d['step']),
42
+ 'type': d['type'],
43
+ 'amount': amount,
44
+ 'oldbalanceOrg': old_org,
45
+ 'newbalanceOrig': new_org,
46
+ 'oldbalanceDest': old_dest,
47
+ 'newbalanceDest': new_dest,
48
+ 'errorBalanceOrig': err_orig,
49
+ 'errorBalanceDest': err_dest,
50
+ }])
51
+
52
+ X = preprocessor.transform(df)
53
+ proba = model.predict_proba(X)[0]
54
+
55
+ return jsonify({
56
+ 'legit_prob': round(float(proba[0]) * 100, 2),
57
+ 'fraud_prob': round(float(proba[1]) * 100, 2),
58
+ 'error_balance_orig': round(err_orig, 2),
59
+ 'error_balance_dest': round(err_dest, 2),
60
+ })
61
+ except Exception as e:
62
+ return jsonify({'error': str(e)}), 400
63
+
64
+ if __name__ == '__main__':
65
+ app.run(host='0.0.0.0', port=5000, debug=False)
model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63965f984b9276fd8fd991451b11a5792b164d4eb2cc2b4a5a96c534c786d48e
3
+ size 2433937
preprocessor.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:82dd548d6f8cd947fcbe2b8323346a84813518b978165475d0d3612ca9438b24
3
+ size 1955
requirements.txt ADDED
Binary file (5.76 kB). View file
 
templates/index.html ADDED
@@ -0,0 +1,789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FraudGuard β€” Transaction Risk Analyzer</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #0a0b0f;
11
+ --surface: #111318;
12
+ --surface2: #181b23;
13
+ --border: #252830;
14
+ --text: #e8eaf0;
15
+ --muted: #6b7280;
16
+ --accent: #00e5a0;
17
+ --accent2: #00b8ff;
18
+ --danger: #ff4566;
19
+ --warn: #ffb347;
20
+ --safe: #00e5a0;
21
+ --mono: 'Space Mono', monospace;
22
+ --sans: 'Syne', sans-serif;
23
+ }
24
+
25
+ * { margin: 0; padding: 0; box-sizing: border-box; }
26
+
27
+ body {
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ font-family: var(--sans);
31
+ min-height: 100vh;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ body::before {
36
+ content: '';
37
+ position: fixed;
38
+ inset: 0;
39
+ background-image:
40
+ linear-gradient(rgba(0,229,160,0.03) 1px, transparent 1px),
41
+ linear-gradient(90deg, rgba(0,229,160,0.03) 1px, transparent 1px);
42
+ background-size: 40px 40px;
43
+ pointer-events: none;
44
+ z-index: 0;
45
+ }
46
+
47
+ .container {
48
+ position: relative;
49
+ z-index: 1;
50
+ max-width: 1100px;
51
+ margin: 0 auto;
52
+ padding: 40px 24px 80px;
53
+ }
54
+
55
+ header {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 16px;
59
+ margin-bottom: 48px;
60
+ }
61
+
62
+ .logo-mark {
63
+ width: 48px; height: 48px;
64
+ border: 2px solid var(--accent);
65
+ border-radius: 12px;
66
+ display: flex; align-items: center; justify-content: center;
67
+ font-family: var(--mono);
68
+ font-size: 20px;
69
+ color: var(--accent);
70
+ position: relative;
71
+ overflow: hidden;
72
+ }
73
+
74
+ .logo-mark::after {
75
+ content: '';
76
+ position: absolute;
77
+ inset: 0;
78
+ background: linear-gradient(135deg, rgba(0,229,160,0.15), transparent);
79
+ }
80
+
81
+ .header-text h1 {
82
+ font-size: 26px;
83
+ font-weight: 800;
84
+ letter-spacing: -0.5px;
85
+ }
86
+
87
+ .header-text h1 span { color: var(--accent); }
88
+
89
+ .header-text p {
90
+ font-size: 13px;
91
+ color: var(--muted);
92
+ font-family: var(--mono);
93
+ margin-top: 2px;
94
+ }
95
+
96
+ .badge {
97
+ margin-left: auto;
98
+ background: rgba(0,229,160,0.1);
99
+ border: 1px solid rgba(0,229,160,0.3);
100
+ color: var(--accent);
101
+ font-family: var(--mono);
102
+ font-size: 11px;
103
+ padding: 4px 10px;
104
+ border-radius: 4px;
105
+ letter-spacing: 1px;
106
+ }
107
+
108
+ .layout {
109
+ display: grid;
110
+ grid-template-columns: 1fr 380px;
111
+ gap: 24px;
112
+ align-items: start;
113
+ }
114
+
115
+ @media (max-width: 820px) {
116
+ .layout { grid-template-columns: 1fr; }
117
+ }
118
+
119
+ .card {
120
+ background: var(--surface);
121
+ border: 1px solid var(--border);
122
+ border-radius: 16px;
123
+ padding: 28px;
124
+ }
125
+
126
+ .card-title {
127
+ font-size: 11px;
128
+ font-family: var(--mono);
129
+ letter-spacing: 2px;
130
+ color: var(--muted);
131
+ text-transform: uppercase;
132
+ margin-bottom: 24px;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 8px;
136
+ }
137
+
138
+ .card-title::before {
139
+ content: '';
140
+ width: 16px; height: 2px;
141
+ background: var(--accent);
142
+ display: inline-block;
143
+ }
144
+
145
+ .form-grid {
146
+ display: grid;
147
+ grid-template-columns: 1fr 1fr;
148
+ gap: 16px;
149
+ }
150
+
151
+ .form-group {
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 6px;
155
+ }
156
+
157
+ .form-group.full { grid-column: 1 / -1; }
158
+
159
+ label {
160
+ font-size: 11px;
161
+ font-family: var(--mono);
162
+ color: var(--muted);
163
+ letter-spacing: 0.5px;
164
+ text-transform: uppercase;
165
+ }
166
+
167
+ label .hint {
168
+ font-size: 10px;
169
+ color: #3d4150;
170
+ font-style: normal;
171
+ margin-left: 4px;
172
+ }
173
+
174
+ input, select {
175
+ background: var(--surface2);
176
+ border: 1px solid var(--border);
177
+ border-radius: 8px;
178
+ color: var(--text);
179
+ font-family: var(--mono);
180
+ font-size: 13px;
181
+ padding: 10px 12px;
182
+ transition: border-color 0.2s, box-shadow 0.2s;
183
+ outline: none;
184
+ width: 100%;
185
+ }
186
+
187
+ input:focus, select:focus {
188
+ border-color: var(--accent);
189
+ box-shadow: 0 0 0 3px rgba(0,229,160,0.1);
190
+ }
191
+
192
+ select option { background: var(--surface2); }
193
+
194
+ .error-calc {
195
+ background: rgba(0,184,255,0.06);
196
+ border: 1px solid rgba(0,184,255,0.2);
197
+ border-radius: 8px;
198
+ padding: 10px 14px;
199
+ font-size: 11px;
200
+ font-family: var(--mono);
201
+ color: var(--accent2);
202
+ grid-column: 1 / -1;
203
+ line-height: 1.6;
204
+ }
205
+
206
+ .error-calc strong { color: var(--text); display: block; margin-bottom: 3px; }
207
+
208
+ .computed-row {
209
+ grid-column: 1 / -1;
210
+ display: grid;
211
+ grid-template-columns: 1fr 1fr;
212
+ gap: 16px;
213
+ }
214
+
215
+ .computed-field {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 6px;
219
+ }
220
+
221
+ .computed-field input {
222
+ opacity: 0.55;
223
+ cursor: not-allowed;
224
+ }
225
+
226
+ .btn-analyze {
227
+ width: 100%;
228
+ background: var(--accent);
229
+ color: #000;
230
+ border: none;
231
+ border-radius: 10px;
232
+ padding: 14px;
233
+ font-family: var(--sans);
234
+ font-size: 15px;
235
+ font-weight: 800;
236
+ letter-spacing: 0.5px;
237
+ cursor: pointer;
238
+ margin-top: 20px;
239
+ transition: opacity 0.2s, transform 0.1s, box-shadow 0.2s;
240
+ position: relative;
241
+ overflow: hidden;
242
+ }
243
+
244
+ .btn-analyze:hover {
245
+ opacity: 0.92;
246
+ box-shadow: 0 0 24px rgba(0,229,160,0.4);
247
+ }
248
+
249
+ .btn-analyze:active { transform: scale(0.99); }
250
+ .btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
251
+
252
+ .result-panel {
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: 20px;
256
+ }
257
+
258
+ .meter-card {
259
+ background: var(--surface);
260
+ border: 1px solid var(--border);
261
+ border-radius: 16px;
262
+ padding: 28px 28px 24px;
263
+ text-align: center;
264
+ transition: border-color 0.4s;
265
+ }
266
+
267
+ .meter-wrap {
268
+ position: relative;
269
+ width: 200px;
270
+ height: 110px;
271
+ margin: 0 auto 16px;
272
+ }
273
+
274
+ .meter-svg { width: 200px; height: 110px; }
275
+
276
+ .meter-value {
277
+ position: absolute;
278
+ bottom: 0; left: 0; right: 0;
279
+ font-family: var(--mono);
280
+ font-size: 32px;
281
+ font-weight: 700;
282
+ color: var(--text);
283
+ text-align: center;
284
+ line-height: 1;
285
+ transition: color 0.4s;
286
+ }
287
+
288
+ .meter-label {
289
+ font-size: 10px;
290
+ font-family: var(--mono);
291
+ color: var(--muted);
292
+ letter-spacing: 2px;
293
+ text-transform: uppercase;
294
+ margin-top: 4px;
295
+ }
296
+
297
+ .verdict {
298
+ display: inline-flex;
299
+ align-items: center;
300
+ gap: 8px;
301
+ padding: 8px 20px;
302
+ border-radius: 8px;
303
+ font-weight: 700;
304
+ font-size: 15px;
305
+ letter-spacing: 0.5px;
306
+ margin-top: 12px;
307
+ transition: all 0.4s;
308
+ }
309
+
310
+ .verdict.safe { background: rgba(0,229,160,0.12); color: var(--safe); border: 1px solid rgba(0,229,160,0.3); }
311
+ .verdict.danger { background: rgba(255,69,102,0.12); color: var(--danger); border: 1px solid rgba(255,69,102,0.3); }
312
+ .verdict.warn { background: rgba(255,179,71,0.12); color: var(--warn); border: 1px solid rgba(255,179,71,0.3); }
313
+ .verdict.idle { background: rgba(107,114,128,0.1); color: var(--muted); border: 1px solid var(--border); }
314
+
315
+ .conf-bars { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; }
316
+
317
+ .bar-row {
318
+ display: grid;
319
+ grid-template-columns: 80px 1fr 48px;
320
+ align-items: center;
321
+ gap: 10px;
322
+ font-size: 12px;
323
+ font-family: var(--mono);
324
+ }
325
+
326
+ .bar-label { color: var(--muted); }
327
+
328
+ .bar-track {
329
+ height: 6px;
330
+ background: var(--border);
331
+ border-radius: 3px;
332
+ overflow: hidden;
333
+ }
334
+
335
+ .bar-fill {
336
+ height: 100%;
337
+ border-radius: 3px;
338
+ transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
339
+ width: 0%;
340
+ }
341
+
342
+ .bar-fill.legit { background: var(--safe); }
343
+ .bar-fill.fraud { background: var(--danger); }
344
+ .bar-pct { color: var(--text); text-align: right; }
345
+
346
+ .presets-card .card-title { margin-bottom: 16px; }
347
+ .preset-list { display: flex; flex-direction: column; gap: 8px; }
348
+
349
+ .preset-btn {
350
+ background: var(--surface2);
351
+ border: 1px solid var(--border);
352
+ border-radius: 8px;
353
+ padding: 10px 14px;
354
+ cursor: pointer;
355
+ text-align: left;
356
+ transition: border-color 0.2s, background 0.2s;
357
+ display: flex;
358
+ justify-content: space-between;
359
+ align-items: center;
360
+ }
361
+
362
+ .preset-btn:hover {
363
+ border-color: var(--accent);
364
+ background: rgba(0,229,160,0.04);
365
+ }
366
+
367
+ .preset-name {
368
+ font-family: var(--sans);
369
+ font-size: 13px;
370
+ font-weight: 600;
371
+ color: var(--text);
372
+ }
373
+
374
+ .preset-desc {
375
+ font-size: 11px;
376
+ font-family: var(--mono);
377
+ color: var(--muted);
378
+ margin-top: 2px;
379
+ }
380
+
381
+ .preset-tag {
382
+ font-size: 10px;
383
+ font-family: var(--mono);
384
+ padding: 3px 8px;
385
+ border-radius: 4px;
386
+ letter-spacing: 0.5px;
387
+ flex-shrink: 0;
388
+ }
389
+
390
+ .preset-tag.fraud { background: rgba(255,69,102,0.15); color: var(--danger); }
391
+ .preset-tag.legit { background: rgba(0,229,160,0.12); color: var(--safe); }
392
+
393
+ .breakdown-card { display: none; }
394
+ .breakdown-card.show { display: block; }
395
+
396
+ .feature-grid {
397
+ display: grid;
398
+ grid-template-columns: 1fr 1fr;
399
+ gap: 8px;
400
+ font-size: 11px;
401
+ font-family: var(--mono);
402
+ }
403
+
404
+ .feature-item {
405
+ background: var(--surface2);
406
+ border-radius: 6px;
407
+ padding: 8px 10px;
408
+ display: flex;
409
+ justify-content: space-between;
410
+ align-items: center;
411
+ }
412
+
413
+ .feature-key { color: var(--muted); }
414
+ .feature-val { color: var(--text); font-weight: 700; }
415
+
416
+ @keyframes pulse-danger {
417
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255,69,102,0.4); }
418
+ 50% { box-shadow: 0 0 0 8px rgba(255,69,102,0); }
419
+ }
420
+
421
+ .meter-card.fraud-alert {
422
+ animation: pulse-danger 1.5s ease-in-out 3;
423
+ border-color: rgba(255,69,102,0.4);
424
+ }
425
+
426
+ @keyframes scan {
427
+ from { transform: translateY(-100%); }
428
+ to { transform: translateY(100%); }
429
+ }
430
+
431
+ .scanning::after {
432
+ content: '';
433
+ position: absolute;
434
+ top: 0; left: 0; right: 0;
435
+ height: 2px;
436
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
437
+ animation: scan 0.4s ease-in-out;
438
+ }
439
+
440
+ .error-msg {
441
+ display: none;
442
+ background: rgba(255,69,102,0.08);
443
+ border: 1px solid rgba(255,69,102,0.3);
444
+ border-radius: 8px;
445
+ padding: 10px 14px;
446
+ font-size: 12px;
447
+ font-family: var(--mono);
448
+ color: var(--danger);
449
+ margin-top: 12px;
450
+ grid-column: 1/-1;
451
+ }
452
+
453
+ .spinner {
454
+ display: inline-block;
455
+ width: 14px; height: 14px;
456
+ border: 2px solid rgba(0,0,0,0.3);
457
+ border-top-color: #000;
458
+ border-radius: 50%;
459
+ animation: spin 0.6s linear infinite;
460
+ vertical-align: middle;
461
+ margin-right: 6px;
462
+ }
463
+ @keyframes spin { to { transform: rotate(360deg); } }
464
+ </style>
465
+ </head>
466
+ <body>
467
+ <div class="container">
468
+
469
+ <header>
470
+ <div class="logo-mark">⚑</div>
471
+ <div class="header-text">
472
+ <h1>Fraud<span>Guard</span></h1>
473
+ <p>Decision Tree Β· PaySim Dataset Β· Flask Backend</p>
474
+ </div>
475
+ <div class="badge">LIVE MODEL</div>
476
+ </header>
477
+
478
+ <div class="layout">
479
+
480
+ <!-- LEFT: Input Form -->
481
+ <div>
482
+ <div class="card">
483
+ <div class="card-title">Transaction Details</div>
484
+
485
+ <div class="form-grid">
486
+
487
+ <div class="form-group full">
488
+ <label>Transaction Type</label>
489
+ <select id="type">
490
+ <option value="CASH_IN">CASH_IN β€” Deposit into account</option>
491
+ <option value="CASH_OUT" selected>CASH_OUT β€” Withdraw cash</option>
492
+ <option value="DEBIT">DEBIT β€” Direct debit payment</option>
493
+ <option value="PAYMENT">PAYMENT β€” Merchant payment</option>
494
+ <option value="TRANSFER">TRANSFER β€” Transfer to another account</option>
495
+ </select>
496
+ </div>
497
+
498
+ <div class="form-group">
499
+ <label>Step <span class="hint">(hour of sim)</span></label>
500
+ <input type="number" id="step" value="1" min="1" max="744">
501
+ </div>
502
+
503
+ <div class="form-group">
504
+ <label>Amount <span class="hint">($)</span></label>
505
+ <input type="number" id="amount" value="200000" min="0" step="0.01">
506
+ </div>
507
+
508
+ <div class="form-group">
509
+ <label>Sender Old Balance</label>
510
+ <input type="number" id="oldbalanceOrg" value="200000" min="0" step="0.01">
511
+ </div>
512
+
513
+ <div class="form-group">
514
+ <label>Sender New Balance</label>
515
+ <input type="number" id="newbalanceOrig" value="0" min="0" step="0.01">
516
+ </div>
517
+
518
+ <div class="form-group">
519
+ <label>Recipient Old Balance</label>
520
+ <input type="number" id="oldbalanceDest" value="100000" min="0" step="0.01">
521
+ </div>
522
+
523
+ <div class="form-group">
524
+ <label>Recipient New Balance</label>
525
+ <input type="number" id="newbalanceDest" value="300000" min="0" step="0.01">
526
+ </div>
527
+
528
+ <div class="error-calc">
529
+ <strong>βš™ Error Balance Fields β€” auto-computed by server</strong>
530
+ errorBalanceOrig = oldBalanceOrg βˆ’ amount βˆ’ newBalanceOrig<br>
531
+ errorBalanceDest = newBalanceDest βˆ’ oldBalanceDest βˆ’ amount
532
+ </div>
533
+
534
+ <div class="computed-row">
535
+ <div class="computed-field form-group">
536
+ <label>errorBalanceOrig</label>
537
+ <input type="number" id="errorBalanceOrig" value="β€”" readonly>
538
+ </div>
539
+ <div class="computed-field form-group">
540
+ <label>errorBalanceDest</label>
541
+ <input type="number" id="errorBalanceDest" value="β€”" readonly>
542
+ </div>
543
+ </div>
544
+
545
+ <div class="error-msg" id="errorMsg"></div>
546
+
547
+ </div>
548
+
549
+ <button class="btn-analyze" onclick="analyze()" id="analyzeBtn">
550
+ ⚑ Analyze Transaction
551
+ </button>
552
+ </div>
553
+ </div>
554
+
555
+ <!-- RIGHT: Results -->
556
+ <div class="result-panel">
557
+
558
+ <!-- Risk Meter -->
559
+ <div class="meter-card" id="meterCard">
560
+ <div class="card-title" style="justify-content:center">Risk Score</div>
561
+
562
+ <div class="meter-wrap">
563
+ <svg class="meter-svg" viewBox="0 0 200 110" xmlns="http://www.w3.org/2000/svg">
564
+ <path d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke="#252830" stroke-width="12" stroke-linecap="round"/>
565
+ <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"/>
566
+ <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"/>
567
+ <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"
568
+ style="transition: all 0.6s cubic-bezier(0.4,0,0.2,1)"/>
569
+ <line id="meterNeedle" x1="100" y1="100" x2="20" y2="100"
570
+ stroke="#e8eaf0" stroke-width="2" stroke-linecap="round"
571
+ style="transform-origin: 100px 100px; transition: transform 0.6s cubic-bezier(0.4,0,0.2,1); transform: rotate(0deg)"/>
572
+ <circle cx="100" cy="100" r="5" fill="#e8eaf0"/>
573
+ </svg>
574
+ <div class="meter-value" id="meterValue">β€”</div>
575
+ </div>
576
+
577
+ <div class="meter-label">FRAUD PROBABILITY</div>
578
+
579
+ <div id="verdictWrap" style="margin-top:12px">
580
+ <div class="verdict idle">⬑ Awaiting analysis</div>
581
+ </div>
582
+
583
+ <div class="conf-bars">
584
+ <div class="bar-row">
585
+ <span class="bar-label">Legitimate</span>
586
+ <div class="bar-track"><div class="bar-fill legit" id="barLegit"></div></div>
587
+ <span class="bar-pct" id="pctLegit">β€”</span>
588
+ </div>
589
+ <div class="bar-row">
590
+ <span class="bar-label">Fraudulent</span>
591
+ <div class="bar-track"><div class="bar-fill fraud" id="barFraud"></div></div>
592
+ <span class="bar-pct" id="pctFraud">β€”</span>
593
+ </div>
594
+ </div>
595
+ </div>
596
+
597
+ <!-- Feature Snapshot -->
598
+ <div class="card breakdown-card" id="breakdownCard">
599
+ <div class="card-title">Input Summary</div>
600
+ <div class="feature-grid" id="featureGrid"></div>
601
+ </div>
602
+
603
+ <!-- Preset Examples -->
604
+ <div class="card presets-card">
605
+ <div class="card-title">Example Transactions</div>
606
+ <div class="preset-list">
607
+
608
+ <div class="preset-btn" onclick="loadPreset('fraud_cashout')">
609
+ <div>
610
+ <div class="preset-name">Account Drain (CASH_OUT)</div>
611
+ <div class="preset-desc">Full balance withdrawn, large amount</div>
612
+ </div>
613
+ <span class="preset-tag fraud">FRAUD</span>
614
+ </div>
615
+
616
+ <div class="preset-btn" onclick="loadPreset('fraud_transfer')">
617
+ <div>
618
+ <div class="preset-name">Ghost Transfer</div>
619
+ <div class="preset-desc">Transfer zeroing originator balance</div>
620
+ </div>
621
+ <span class="preset-tag fraud">FRAUD</span>
622
+ </div>
623
+
624
+ <div class="preset-btn" onclick="loadPreset('legit_payment')">
625
+ <div>
626
+ <div class="preset-name">Routine Payment</div>
627
+ <div class="preset-desc">Small merchant payment, normal balances</div>
628
+ </div>
629
+ <span class="preset-tag legit">LEGIT</span>
630
+ </div>
631
+
632
+ <div class="preset-btn" onclick="loadPreset('legit_cashin')">
633
+ <div>
634
+ <div class="preset-name">Salary Deposit</div>
635
+ <div class="preset-desc">CASH_IN, balance increases correctly</div>
636
+ </div>
637
+ <span class="preset-tag legit">LEGIT</span>
638
+ </div>
639
+
640
+ </div>
641
+ </div>
642
+
643
+ </div>
644
+ </div>
645
+ </div>
646
+
647
+ <script>
648
+ // ─── Meter rendering ─────────────────────────────────────────────────────────
649
+ function setMeter(prob) {
650
+ const angle = prob * 180;
651
+ const needleAngle = -90 + angle;
652
+ document.getElementById('meterNeedle').style.transform = `rotate(${needleAngle}deg)`;
653
+
654
+ const cx = 100, cy = 100, r = 80;
655
+ const startAngle = Math.PI;
656
+ const endAngle = Math.PI - (prob * Math.PI);
657
+ const x1 = cx + r * Math.cos(startAngle);
658
+ const y1 = cy + r * Math.sin(startAngle);
659
+ const x2 = cx + r * Math.cos(endAngle);
660
+ const y2 = cy + r * Math.sin(endAngle);
661
+
662
+ let color;
663
+ if (prob < 0.3) color = '#00e5a0';
664
+ else if (prob < 0.6) color = '#ffb347';
665
+ else color = '#ff4566';
666
+
667
+ const arcEl = document.getElementById('meterArc');
668
+ if (prob === 0) {
669
+ arcEl.setAttribute('d', `M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x1} ${y1}`);
670
+ } else {
671
+ const la = angle > 180 ? 1 : 0;
672
+ arcEl.setAttribute('d', `M ${x1} ${y1} A ${r} ${r} 0 ${la} 1 ${x2} ${y2}`);
673
+ }
674
+ arcEl.setAttribute('stroke', color);
675
+ document.getElementById('meterValue').textContent = (prob * 100).toFixed(1) + '%';
676
+ document.getElementById('meterValue').style.color = color;
677
+ }
678
+
679
+ // ─── Analyze via Flask /predict ─────────────────────────────────────────────
680
+ async function analyze() {
681
+ const btn = document.getElementById('analyzeBtn');
682
+ const errDiv = document.getElementById('errorMsg');
683
+ errDiv.style.display = 'none';
684
+
685
+ btn.disabled = true;
686
+ btn.innerHTML = '<span class="spinner"></span>Analyzing…';
687
+ btn.classList.add('scanning');
688
+ setTimeout(() => btn.classList.remove('scanning'), 500);
689
+
690
+ const payload = {
691
+ step: document.getElementById('step').value,
692
+ type: document.getElementById('type').value,
693
+ amount: document.getElementById('amount').value,
694
+ oldbalanceOrg: document.getElementById('oldbalanceOrg').value,
695
+ newbalanceOrig: document.getElementById('newbalanceOrig').value,
696
+ oldbalanceDest: document.getElementById('oldbalanceDest').value,
697
+ newbalanceDest: document.getElementById('newbalanceDest').value,
698
+ };
699
+
700
+ try {
701
+ const res = await fetch('/predict', {
702
+ method: 'POST',
703
+ headers: { 'Content-Type': 'application/json' },
704
+ body: JSON.stringify(payload),
705
+ });
706
+ const data = await res.json();
707
+ if (data.error) throw new Error(data.error);
708
+
709
+ const fraudProb = data.fraud_prob / 100;
710
+ const legitProb = data.legit_prob / 100;
711
+
712
+ // Update computed error fields
713
+ document.getElementById('errorBalanceOrig').value = data.error_balance_orig;
714
+ document.getElementById('errorBalanceDest').value = data.error_balance_dest;
715
+
716
+ // Meter
717
+ setMeter(fraudProb);
718
+
719
+ // Bars
720
+ document.getElementById('barLegit').style.width = data.legit_prob + '%';
721
+ document.getElementById('barFraud').style.width = data.fraud_prob + '%';
722
+ document.getElementById('pctLegit').textContent = data.legit_prob.toFixed(1) + '%';
723
+ document.getElementById('pctFraud').textContent = data.fraud_prob.toFixed(1) + '%';
724
+
725
+ // Verdict
726
+ const meterCard = document.getElementById('meterCard');
727
+ meterCard.classList.remove('fraud-alert');
728
+ let verdictHtml;
729
+ if (fraudProb >= 0.7) {
730
+ verdictHtml = `<div class="verdict danger">⚠ HIGH FRAUD RISK</div>`;
731
+ setTimeout(() => meterCard.classList.add('fraud-alert'), 100);
732
+ } else if (fraudProb >= 0.3) {
733
+ verdictHtml = `<div class="verdict warn">β—ˆ SUSPICIOUS β€” Review</div>`;
734
+ } else {
735
+ verdictHtml = `<div class="verdict safe">βœ“ LIKELY LEGITIMATE</div>`;
736
+ }
737
+ document.getElementById('verdictWrap').innerHTML = verdictHtml;
738
+
739
+ // Feature snapshot
740
+ const snap = [
741
+ ['type', payload.type],
742
+ ['step', payload.step],
743
+ ['amount', (+payload.amount).toLocaleString()],
744
+ ['senderOld', (+payload.oldbalanceOrg).toLocaleString()],
745
+ ['senderNew', (+payload.newbalanceOrig).toLocaleString()],
746
+ ['recipOld', (+payload.oldbalanceDest).toLocaleString()],
747
+ ['recipNew', (+payload.newbalanceDest).toLocaleString()],
748
+ ['errOrig', data.error_balance_orig],
749
+ ['errDest', data.error_balance_dest],
750
+ ];
751
+ document.getElementById('featureGrid').innerHTML = snap.map(([k, v]) =>
752
+ `<div class="feature-item">
753
+ <span class="feature-key">${k}</span>
754
+ <span class="feature-val">${v}</span>
755
+ </div>`
756
+ ).join('');
757
+ document.getElementById('breakdownCard').classList.add('show');
758
+
759
+ } catch (err) {
760
+ errDiv.textContent = '⚠ ' + err.message;
761
+ errDiv.style.display = 'block';
762
+ } finally {
763
+ btn.disabled = false;
764
+ btn.innerHTML = '⚑ Analyze Transaction';
765
+ }
766
+ }
767
+
768
+ // ─── Presets ─────────────────────────────────────────────────────────────────
769
+ const PRESETS = {
770
+ fraud_cashout: { step:100, type:'CASH_OUT', amount:200000, oldbalanceOrg:200000, newbalanceOrig:0, oldbalanceDest:100000, newbalanceDest:300000 },
771
+ fraud_transfer: { step:1, type:'TRANSFER', amount:9839.64, oldbalanceOrg:170136, newbalanceOrig:0, oldbalanceDest:0, newbalanceDest:9839.64 },
772
+ legit_payment: { step:50, type:'PAYMENT', amount:500, oldbalanceOrg:10000, newbalanceOrig:9500, oldbalanceDest:0, newbalanceDest:0 },
773
+ legit_cashin: { step:30, type:'CASH_IN', amount:5000, oldbalanceOrg:1000, newbalanceOrig:6000, oldbalanceDest:80000, newbalanceDest:75000 },
774
+ };
775
+
776
+ function loadPreset(key) {
777
+ const p = PRESETS[key];
778
+ document.getElementById('type').value = p.type;
779
+ document.getElementById('step').value = p.step;
780
+ document.getElementById('amount').value = p.amount;
781
+ document.getElementById('oldbalanceOrg').value = p.oldbalanceOrg;
782
+ document.getElementById('newbalanceOrig').value = p.newbalanceOrig;
783
+ document.getElementById('oldbalanceDest').value = p.oldbalanceDest;
784
+ document.getElementById('newbalanceDest').value = p.newbalanceDest;
785
+ analyze();
786
+ }
787
+ </script>
788
+ </body>
789
+ </html>