Spaces:
Runtime error
Runtime error
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Fraud Detection System</title> | |
| <!-- PWA: manifest & theme --> | |
| <link rel="manifest" href="/manifest.json"> | |
| <meta name="theme-color" content="#3498db"> | |
| <link rel="icon" href="/icons/icon.svg" sizes="192x192"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #2c3e50; | |
| --secondary-color: #3498db; | |
| --success-color: #27ae60; | |
| --danger-color: #e74c3c; | |
| --warning-color: #f39c12; | |
| --info-color: #17a2b8; | |
| --light-bg: #f8f9fa; | |
| --dark-text: #2c3e50; | |
| --border-radius: 10px; | |
| --box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
| --transition: all 0.3s ease; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: #f5f7fa; | |
| color: var(--dark-text); | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| /* Header Styles */ | |
| header { | |
| background-color: white; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 15px 0; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .logo-icon { | |
| background-color: var(--primary-color); | |
| color: white; | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| } | |
| .logo-text h1 { | |
| font-size: 1.8rem; | |
| color: var(--primary-color); | |
| } | |
| .logo-text p { | |
| font-size: 0.9rem; | |
| color: #666; | |
| } | |
| nav ul { | |
| display: flex; | |
| list-style: none; | |
| gap: 25px; | |
| } | |
| nav a { | |
| text-decoration: none; | |
| color: var(--dark-text); | |
| font-weight: 500; | |
| padding: 8px 15px; | |
| border-radius: var(--border-radius); | |
| transition: var(--transition); | |
| } | |
| nav a:hover, nav a.active { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .user-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .user-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .user-avatar { | |
| width: 40px; | |
| height: 40px; | |
| background-color: var(--secondary-color); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| } | |
| .logout-btn { | |
| background-color: var(--danger-color); | |
| color: white; | |
| border: none; | |
| padding: 8px 20px; | |
| border-radius: var(--border-radius); | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: var(--transition); | |
| } | |
| .logout-btn:hover { | |
| background-color: #c0392b; | |
| transform: translateY(-2px); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 30px; | |
| margin-top: 30px; | |
| } | |
| /* Dashboard Cards */ | |
| .dashboard-cards { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 25px; | |
| } | |
| .card { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 25px; | |
| box-shadow: var(--box-shadow); | |
| transition: var(--transition); | |
| border-top: 4px solid transparent; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); | |
| } | |
| .card.fraud { | |
| border-top-color: var(--danger-color); | |
| } | |
| .card.safe { | |
| border-top-color: var(--success-color); | |
| } | |
| .card.warning { | |
| border-top-color: var(--warning-color); | |
| } | |
| .card.info { | |
| border-top-color: var(--info-color); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .card-icon { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.8rem; | |
| color: white; | |
| } | |
| .fraud .card-icon { | |
| background-color: var(--danger-color); | |
| } | |
| .safe .card-icon { | |
| background-color: var(--success-color); | |
| } | |
| .warning .card-icon { | |
| background-color: var(--warning-color); | |
| } | |
| .info .card-icon { | |
| background-color: var(--info-color); | |
| } | |
| .card-value { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| margin-bottom: 5px; | |
| } | |
| .card-label { | |
| color: #666; | |
| font-size: 1rem; | |
| } | |
| /* Section Header */ | |
| .section-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 25px; | |
| } | |
| .section-title { | |
| font-size: 1.8rem; | |
| color: var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .section-title i { | |
| color: var(--secondary-color); | |
| } | |
| /* Prediction Form */ | |
| .prediction-form-container { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 30px; | |
| box-shadow: var(--box-shadow); | |
| } | |
| .form-row { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 25px; | |
| margin-bottom: 25px; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| .form-control { | |
| width: 100%; | |
| padding: 14px 20px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: var(--border-radius); | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| background-color: #f9f9f9; | |
| } | |
| .form-control:focus { | |
| outline: none; | |
| border-color: var(--secondary-color); | |
| background-color: white; | |
| box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); | |
| } | |
| .form-note { | |
| font-size: 0.9rem; | |
| color: #666; | |
| margin-top: 5px; | |
| font-style: italic; | |
| } | |
| .btn { | |
| padding: 14px 30px; | |
| border: none; | |
| border-radius: var(--border-radius); | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .btn-primary { | |
| background-color: var(--secondary-color); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: #2980b9; | |
| transform: translateY(-2px); | |
| } | |
| .btn-success { | |
| background-color: var(--success-color); | |
| color: white; | |
| } | |
| .btn-success:hover { | |
| background-color: #219653; | |
| } | |
| .btn-danger { | |
| background-color: var(--danger-color); | |
| color: white; | |
| } | |
| .btn-danger:hover { | |
| background-color: #c0392b; | |
| } | |
| .btn-block { | |
| display: block; | |
| width: 100%; | |
| } | |
| /* Results Section */ | |
| .results-container { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 30px; | |
| box-shadow: var(--box-shadow); | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .results-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 25px; | |
| padding-bottom: 15px; | |
| border-bottom: 2px solid #f0f0f0; | |
| } | |
| .results-title { | |
| font-size: 1.6rem; | |
| color: var(--primary-color); | |
| } | |
| .prediction-badge { | |
| padding: 10px 25px; | |
| border-radius: 50px; | |
| font-weight: 700; | |
| font-size: 1.1rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .badge-fraud { | |
| background-color: rgba(231, 76, 60, 0.1); | |
| color: var(--danger-color); | |
| border: 2px solid var(--danger-color); | |
| } | |
| .badge-safe { | |
| background-color: rgba(39, 174, 96, 0.1); | |
| color: var(--success-color); | |
| border: 2px solid var(--success-color); | |
| } | |
| .prediction-details { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 25px; | |
| margin-bottom: 30px; | |
| } | |
| .detail-box { | |
| background-color: #f8f9fa; | |
| padding: 20px; | |
| border-radius: var(--border-radius); | |
| border-left: 4px solid var(--secondary-color); | |
| } | |
| .detail-label { | |
| font-size: 0.9rem; | |
| color: #666; | |
| margin-bottom: 5px; | |
| } | |
| .detail-value { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| } | |
| .probability-container { | |
| margin: 30px 0; | |
| } | |
| .probability-header { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 10px; | |
| } | |
| .probability-bar { | |
| height: 25px; | |
| background-color: #ecf0f1; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| margin-bottom: 15px; | |
| } | |
| .probability-fill { | |
| height: 100%; | |
| border-radius: 12px; | |
| transition: width 1s ease; | |
| } | |
| .fraud-probability { | |
| background: linear-gradient(90deg, #e74c3c, #c0392b); | |
| } | |
| .safe-probability { | |
| background: linear-gradient(90deg, #27ae60, #219653); | |
| } | |
| .probability-text { | |
| display: flex; | |
| justify-content: space-between; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| .feedback-section { | |
| margin-top: 35px; | |
| padding-top: 25px; | |
| border-top: 2px solid #f0f0f0; | |
| } | |
| .feedback-title { | |
| font-size: 1.2rem; | |
| margin-bottom: 15px; | |
| color: var(--primary-color); | |
| } | |
| .feedback-buttons { | |
| display: flex; | |
| gap: 15px; | |
| } | |
| .feedback-btn { | |
| flex: 1; | |
| padding: 15px; | |
| border-radius: var(--border-radius); | |
| background-color: #f8f9fa; | |
| border: 2px solid #ddd; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| font-weight: 600; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .feedback-btn:hover { | |
| background-color: #e9ecef; | |
| } | |
| .feedback-btn.active { | |
| background-color: var(--secondary-color); | |
| color: white; | |
| border-color: var(--secondary-color); | |
| } | |
| /* Charts Section */ | |
| .charts-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); | |
| gap: 30px; | |
| margin-bottom: 30px; | |
| } | |
| .chart-card { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 25px; | |
| box-shadow: var(--box-shadow); | |
| } | |
| .chart-title { | |
| font-size: 1.3rem; | |
| margin-bottom: 20px; | |
| color: var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .chart-wrapper { | |
| position: relative; | |
| height: 300px; | |
| } | |
| /* Transactions Table */ | |
| .transactions-container { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 30px; | |
| box-shadow: var(--box-shadow); | |
| margin-bottom: 30px; | |
| } | |
| .table-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 25px; | |
| } | |
| .table-actions { | |
| display: flex; | |
| gap: 15px; | |
| } | |
| .table-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 20px; | |
| padding-top: 20px; | |
| border-top: 1px solid #eee; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 20px; | |
| } | |
| thead { | |
| background-color: #f8f9fa; | |
| } | |
| th { | |
| padding: 16px 20px; | |
| text-align: left; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| border-bottom: 2px solid #eee; | |
| } | |
| td { | |
| padding: 16px 20px; | |
| border-bottom: 1px solid #eee; | |
| } | |
| tr:hover { | |
| background-color: #f9f9f9; | |
| } | |
| .status-badge { | |
| padding: 6px 15px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| text-align: center; | |
| display: inline-block; | |
| } | |
| .status-fraud { | |
| background-color: rgba(231, 76, 60, 0.1); | |
| color: var(--danger-color); | |
| border: 1px solid rgba(231, 76, 60, 0.3); | |
| } | |
| .status-safe { | |
| background-color: rgba(39, 174, 96, 0.1); | |
| color: var(--success-color); | |
| border: 1px solid rgba(39, 174, 96, 0.3); | |
| } | |
| /* Feature Importance */ | |
| .feature-importance-container { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 30px; | |
| box-shadow: var(--box-shadow); | |
| margin-bottom: 30px; | |
| } | |
| .feature-list { | |
| margin-top: 25px; | |
| } | |
| .feature-item { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding: 15px; | |
| background-color: #f8f9fa; | |
| border-radius: var(--border-radius); | |
| border-left: 4px solid var(--secondary-color); | |
| } | |
| .feature-rank { | |
| background-color: var(--primary-color); | |
| color: white; | |
| width: 35px; | |
| height: 35px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| margin-right: 15px; | |
| } | |
| .feature-name { | |
| flex-grow: 1; | |
| font-weight: 600; | |
| } | |
| .feature-importance { | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| } | |
| /* Notification System */ | |
| .notification-container { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 2000; | |
| max-width: 400px; | |
| } | |
| .notification { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 20px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); | |
| margin-bottom: 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| transform: translateX(120%); | |
| transition: transform 0.5s ease; | |
| border-left: 5px solid var(--secondary-color); | |
| } | |
| .notification.show { | |
| transform: translateX(0); | |
| } | |
| .notification.warning { | |
| border-left-color: var(--warning-color); | |
| } | |
| .notification.danger { | |
| border-left-color: var(--danger-color); | |
| } | |
| .notification.success { | |
| border-left-color: var(--success-color); | |
| } | |
| .notification-icon { | |
| font-size: 1.8rem; | |
| } | |
| .notification.warning .notification-icon { | |
| color: var(--warning-color); | |
| } | |
| .notification.danger .notification-icon { | |
| color: var(--danger-color); | |
| } | |
| .notification.success .notification-icon { | |
| color: var(--success-color); | |
| } | |
| .notification-content h4 { | |
| margin-bottom: 5px; | |
| color: var(--primary-color); | |
| } | |
| .notification-content p { | |
| color: #666; | |
| font-size: 0.9rem; | |
| } | |
| /* Login Page */ | |
| .login-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 80vh; | |
| } | |
| .login-card { | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| padding: 40px; | |
| box-shadow: var(--box-shadow); | |
| width: 100%; | |
| max-width: 450px; | |
| } | |
| .login-header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| .login-header h2 { | |
| color: var(--primary-color); | |
| margin-bottom: 10px; | |
| } | |
| .login-header p { | |
| color: #666; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 1200px) { | |
| .charts-container { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .header-content { | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| nav ul { | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 15px; | |
| } | |
| .form-row { | |
| grid-template-columns: 1fr; | |
| } | |
| .dashboard-cards { | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| } | |
| .charts-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .feedback-buttons { | |
| flex-direction: column; | |
| } | |
| .table-header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 15px; | |
| } | |
| .table-actions { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Notification System --> | |
| <div class="notification-container" id="notificationContainer"></div> | |
| <!-- Header Section --> | |
| <header> | |
| <div class="container header-content"> | |
| <div class="logo"> | |
| <div class="logo-icon"> | |
| <i class="fas fa-shield-alt"></i> | |
| </div> | |
| <div class="logo-text"> | |
| <h1>Fraud Detection AI</h1> | |
| <p>Real-time Transaction Monitoring System</p> | |
| </div> | |
| </div> | |
| <nav> | |
| <ul> | |
| <li><a href="#" class="nav-link active" data-page="dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li> | |
| <li><a href="#" class="nav-link" data-page="predict"><i class="fas fa-search"></i> Fraud Prediction</a></li> | |
| <li><a href="#" class="nav-link" data-page="transactions"><i class="fas fa-history"></i> Transactions</a></li> | |
| <li><a href="#" class="nav-link" data-page="features"><i class="fas fa-chart-bar"></i> Feature Analysis</a></li> | |
| </ul> | |
| </nav> | |
| <div class="user-section"> | |
| <div class="user-info"> | |
| <div class="user-avatar">AD</div> | |
| <div> | |
| <div class="user-name">Admin User</div> | |
| <div class="user-role" style="font-size: 0.85rem; color: #666;">Administrator</div> | |
| </div> | |
| </div> | |
| <!-- Backend URL control: paste your ngrok backend HTTPS URL here and click Set --> | |
| <div style="display:flex; align-items:center; gap:8px; margin-right:12px;"> | |
| <input id="backendUrlInput" class="form-control" placeholder="Backend URL (https://...)" style="width:320px; padding:8px 12px; font-size:0.9rem;" /> | |
| <button id="setBackendUrlBtn" class="btn" style="background:#444; color:white; padding:8px 12px; border-radius:8px;">Set</button> | |
| </div> | |
| <button class="logout-btn" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> Logout</button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content Container --> | |
| <div class="container"> | |
| <!-- Dashboard Page --> | |
| <div id="dashboard-page" class="page active"> | |
| <div class="main-content"> | |
| <!-- Dashboard Stats --> | |
| <div class="dashboard-cards"> | |
| <div class="card fraud"> | |
| <div class="card-header"> | |
| <div> | |
| <div class="card-value" id="totalFraud">0</div> | |
| <div class="card-label">Fraudulent Transactions</div> | |
| </div> | |
| <div class="card-icon"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| </div> | |
| </div> | |
| <div class="card-trend" style="color: var(--danger-color); font-weight: 600;"> | |
| <i class="fas fa-arrow-up"></i> 12% from last month | |
| </div> | |
| </div> | |
| <div class="card safe"> | |
| <div class="card-header"> | |
| <div> | |
| <div class="card-value" id="totalSafe">0</div> | |
| <div class="card-label">Legitimate Transactions</div> | |
| </div> | |
| <div class="card-icon"> | |
| <i class="fas fa-check-circle"></i> | |
| </div> | |
| </div> | |
| <div class="card-trend" style="color: var(--success-color); font-weight: 600;"> | |
| <i class="fas fa-arrow-up"></i> 8% from last month | |
| </div> | |
| </div> | |
| <div class="card warning"> | |
| <div class="card-header"> | |
| <div> | |
| <div class="card-value" id="totalTransactions">0</div> | |
| <div class="card-label">Total Transactions</div> | |
| </div> | |
| <div class="card-icon"> | |
| <i class="fas fa-exchange-alt"></i> | |
| </div> | |
| </div> | |
| <div class="card-trend" style="color: var(--warning-color); font-weight: 600;"> | |
| <i class="fas fa-arrow-up"></i> 15% from last month | |
| </div> | |
| </div> | |
| <div class="card info"> | |
| <div class="card-header"> | |
| <div> | |
| <div class="card-value" id="accuracyRate">0%</div> | |
| <div class="card-label">Model Accuracy</div> | |
| </div> | |
| <div class="card-icon"> | |
| <i class="fas fa-brain"></i> | |
| </div> | |
| </div> | |
| <div class="card-trend" style="color: var(--info-color); font-weight: 600;"> | |
| <i class="fas fa-arrow-up"></i> 3% improvement | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts Section --> | |
| <div class="section-header"> | |
| <h2 class="section-title"><i class="fas fa-chart-line"></i> Fraud Analytics Overview</h2> | |
| </div> | |
| <div class="charts-container"> | |
| <div class="chart-card"> | |
| <h3 class="chart-title"><i class="fas fa-chart-pie"></i> Fraud Distribution</h3> | |
| <div class="chart-wrapper"> | |
| <canvas id="fraudDistributionChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <h3 class="chart-title"><i class="fas fa-chart-bar"></i> Fraud by Location</h3> | |
| <div class="chart-wrapper"> | |
| <canvas id="locationFraudChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Recent Fraud Alerts --> | |
| <div class="section-header"> | |
| <h2 class="section-title"><i class="fas fa-bell"></i> Recent Fraud Alerts</h2> | |
| <button class="btn btn-primary" id="viewAllAlerts"> | |
| <i class="fas fa-eye"></i> View All | |
| </button> | |
| </div> | |
| <div class="transactions-container"> | |
| <table id="recentAlertsTable"> | |
| <thead> | |
| <tr> | |
| <th>Transaction ID</th> | |
| <th>Date & Time</th> | |
| <th>Amount ($)</th> | |
| <th>Location</th> | |
| <th>Merchant</th> | |
| <th>Status</th> | |
| <th>Action</th> | |
| </tr> | |
| </thead> | |
| <tbody id="recentAlertsBody"> | |
| <!-- Alerts will be loaded here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Fraud Prediction Page --> | |
| <div id="predict-page" class="page"> | |
| <div class="section-header"> | |
| <h2 class="section-title"><i class="fas fa-search"></i> Fraud Detection Prediction</h2> | |
| <div class="model-info" style="background-color: #e8f4fc; padding: 10px 20px; border-radius: var(--border-radius);"> | |
| <span style="font-weight: 600; color: var(--secondary-color);"> | |
| <i class="fas fa-robot"></i> Model: Ensemble (Random Forest + XGBoost) | |
| </span> | |
| </div> | |
| </div> | |
| <div class="prediction-form-container"> | |
| <form id="predictionForm"> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label for="transactionAmount">Transaction Amount ($)</label> | |
| <input type="number" id="transactionAmount" class="form-control" placeholder="Enter transaction amount" step="0.01" min="0" required> | |
| <div class="form-note">Based on dataset: Amount range from $4.30 to $4189.27</div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="transactionLocation">Transaction Location</label> | |
| <select id="transactionLocation" class="form-control" required> | |
| <option value="">Select Location</option> | |
| <option value="San Antonio">San Antonio</option> | |
| <option value="Dallas">Dallas</option> | |
| <option value="New York">New York</option> | |
| <option value="Philadelphia">Philadelphia</option> | |
| <option value="Phoenix">Phoenix</option> | |
| <option value="Utah">Utah</option> | |
| <option value="Maryland">Maryland</option> | |
| <option value="New Mexico">New Mexico</option> | |
| <option value="South Dakota">South Dakota</option> | |
| <option value="Montana">Montana</option> | |
| <option value="Luar Negeri">International</option> | |
| </select> | |
| <div class="form-note">Locations from the fraud dataset</div> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label for="merchantName">Merchant Name</label> | |
| <input type="text" id="merchantName" class="form-control" placeholder="Enter merchant name"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="transactionCategory">Transaction Category</label> | |
| <select id="transactionCategory" class="form-control"> | |
| <option value="retail">Retail</option> | |
| <option value="travel">Travel</option> | |
| <option value="food">Food & Dining</option> | |
| <option value="entertainment">Entertainment</option> | |
| <option value="digital">Digital Products</option> | |
| <option value="other">Other</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label for="customerEmail">Customer Email Domain</label> | |
| <select id="customerEmail" class="form-control"> | |
| <option value="gmail.com">gmail.com</option> | |
| <option value="yahoo.com">yahoo.com</option> | |
| <option value="outlook.com">outlook.com</option> | |
| <option value="company.com">company.com</option> | |
| <option value="other">Other Domain</option> | |
| </select> | |
| <div class="form-note">From dataset: customerEmail is an important feature</div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="customerDevice">Customer Device</label> | |
| <select id="customerDevice" class="form-control"> | |
| <option value="mobile">Mobile</option> | |
| <option value="desktop">Desktop</option> | |
| <option value="tablet">Tablet</option> | |
| <option value="unknown">Unknown</option> | |
| </select> | |
| <div class="form-note">From dataset: customerDevice is used for fraud detection</div> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="transactionType">Transaction Type</label> | |
| <select id="transactionType" class="form-control"> | |
| <option value="online">Online Purchase</option> | |
| <option value="pos">Point of Sale</option> | |
| <option value="atm">ATM Withdrawal</option> | |
| <option value="transfer">Bank Transfer</option> | |
| </select> | |
| </div> | |
| <button type="submit" class="btn btn-primary btn-block" id="predictButton"> | |
| <i class="fas fa-brain"></i> Analyze Transaction for Fraud | |
| </button> | |
| </form> | |
| </div> | |
| <!-- Results Container --> | |
| <div class="results-container" id="resultsContainer"> | |
| <div class="results-header"> | |
| <h3 class="results-title">Prediction Results</h3> | |
| <div class="prediction-badge" id="predictionBadge">Safe</div> | |
| </div> | |
| <div class="prediction-details"> | |
| <div class="detail-box"> | |
| <div class="detail-label">Transaction Amount</div> | |
| <div class="detail-value" id="resultAmount">$0.00</div> | |
| </div> | |
| <div class="detail-box"> | |
| <div class="detail-label">Location</div> | |
| <div class="detail-value" id="resultLocation">Unknown</div> | |
| </div> | |
| <div class="detail-box"> | |
| <div class="detail-label">Merchant</div> | |
| <div class="detail-value" id="resultMerchant">Unknown</div> | |
| </div> | |
| <div class="detail-box"> | |
| <div class="detail-label">Prediction Confidence</div> | |
| <div class="detail-value" id="resultConfidence">0%</div> | |
| </div> | |
| </div> | |
| <div class="probability-container"> | |
| <div class="probability-header"> | |
| <span>Fraud Probability</span> | |
| <span id="probabilityValue">0%</span> | |
| </div> | |
| <div class="probability-bar"> | |
| <div class="probability-fill" id="probabilityFill" style="width: 0%;"></div> | |
| </div> | |
| <div class="probability-text"> | |
| <span>Legitimate Transaction</span> | |
| <span>Fraudulent Transaction</span> | |
| </div> | |
| </div> | |
| <div class="feedback-section"> | |
| <h4 class="feedback-title">Is this prediction accurate?</h4> | |
| <p style="margin-bottom: 20px; color: #666;">Your feedback helps improve the AI model</p> | |
| <div class="feedback-buttons"> | |
| <button class="feedback-btn" id="feedbackAccurate"> | |
| <i class="fas fa-check-circle"></i> | |
| <span>Accurate Prediction</span> | |
| </button> | |
| <button class="feedback-btn" id="feedbackInaccurate"> | |
| <i class="fas fa-times-circle"></i> | |
| <span>Inaccurate Prediction</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Transactions History Page --> | |
| <div id="transactions-page" class="page"> | |
| <div class="section-header"> | |
| <h2 class="section-title"><i class="fas fa-history"></i> Transaction History</h2> | |
| <div class="table-actions"> | |
| <button class="btn btn-primary" id="refreshTransactions"> | |
| <i class="fas fa-sync-alt"></i> Refresh | |
| </button> | |
| <button class="btn btn-success" id="exportTransactions"> | |
| <i class="fas fa-file-export"></i> Export CSV | |
| </button> | |
| </div> | |
| </div> | |
| <div class="transactions-container"> | |
| <div class="filters" style="margin-bottom: 20px; display: flex; gap: 15px; flex-wrap: wrap;"> | |
| <select id="filterStatus" class="form-control" style="width: auto;"> | |
| <option value="">All Status</option> | |
| <option value="fraud">Fraud</option> | |
| <option value="safe">Safe</option> | |
| </select> | |
| <input type="date" id="filterDate" class="form-control" style="width: auto;"> | |
| <input type="text" id="searchTransaction" class="form-control" placeholder="Search transactions..." style="flex-grow: 1;"> | |
| </div> | |
| <table id="transactionsTable"> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>Date & Time</th> | |
| <th>Amount</th> | |
| <th>Location</th> | |
| <th>Merchant</th> | |
| <th>Category</th> | |
| <th>Status</th> | |
| <th>Confidence</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="transactionsBody"> | |
| <!-- Transactions will be loaded here --> | |
| </tbody> | |
| </table> | |
| <div class="table-controls"> | |
| <div class="table-info" id="tableInfo">Showing 0 transactions</div> | |
| <div class="pagination"> | |
| <button class="btn" id="prevPage" disabled><i class="fas fa-chevron-left"></i> Previous</button> | |
| <span style="margin: 0 15px;" id="pageInfo">Page 1 of 1</span> | |
| <button class="btn" id="nextPage" disabled>Next <i class="fas fa-chevron-right"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Feature Importance Page --> | |
| <div id="features-page" class="page"> | |
| <div class="section-header"> | |
| <h2 class="section-title"><i class="fas fa-chart-bar"></i> Feature Importance Analysis</h2> | |
| <div class="model-info" style="background-color: #e8f4fc; padding: 10px 20px; border-radius: var(--border-radius);"> | |
| <span style="font-weight: 600; color: var(--secondary-color);"> | |
| <i class="fas fa-project-diagram"></i> Based on Random Forest Feature Importance | |
| </span> | |
| </div> | |
| </div> | |
| <div class="feature-importance-container"> | |
| <h3 style="margin-bottom: 20px; color: var(--primary-color);">Top 10 Most Important Features for Fraud Detection</h3> | |
| <div class="chart-wrapper"> | |
| <canvas id="featureImportanceChart"></canvas> | |
| </div> | |
| <div class="feature-list" id="featureList"> | |
| <!-- Feature importance list will be loaded here --> | |
| </div> | |
| </div> | |
| <div class="charts-container"> | |
| <div class="chart-card"> | |
| <h3 class="chart-title"><i class="fas fa-money-bill-wave"></i> Amount Distribution</h3> | |
| <div class="chart-wrapper"> | |
| <canvas id="amountDistributionChart"></canvas> | |
| </div> | |
| <div style="margin-top: 15px; font-size: 0.9rem; color: #666;"> | |
| <p>Dataset statistics: Amount ranges from $4.30 to $4189.27</p> | |
| <p>Average fraud amount: $1,245.67 | Average legitimate amount: $256.43</p> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <h3 class="chart-title"><i class="fas fa-map-marker-alt"></i> Fraud by Location Heatmap</h3> | |
| <div class="chart-wrapper"> | |
| <canvas id="locationHeatmapChart"></canvas> | |
| </div> | |
| <div style="margin-top: 15px; font-size: 0.9rem; color: #666;"> | |
| <p>Top 5 fraud locations: New York, Dallas, Phoenix, San Antonio, Philadelphia</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Base URL for Flask API - can be overridden at runtime via the header input. | |
| // NOTE: backend exposes `/predict` at the server root, not under /api | |
| let API_BASE_URL = localStorage.getItem('API_BASE_URL') || 'http://localhost:5000'; | |
| // State management | |
| let currentUser = { | |
| id: 1, | |
| name: "Admin User", | |
| email: "admin@frauddetection.com" | |
| }; | |
| let transactions = []; | |
| let currentPage = 1; | |
| const transactionsPerPage = 10; | |
| // DOM Elements | |
| const pages = { | |
| dashboard: document.getElementById('dashboard-page'), | |
| predict: document.getElementById('predict-page'), | |
| transactions: document.getElementById('transactions-page'), | |
| features: document.getElementById('features-page') | |
| }; | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| const logoutBtn = document.getElementById('logoutBtn'); | |
| const predictionForm = document.getElementById('predictionForm'); | |
| const predictButton = document.getElementById('predictButton'); | |
| const resultsContainer = document.getElementById('resultsContainer'); | |
| const probabilityFill = document.getElementById('probabilityFill'); | |
| const probabilityValue = document.getElementById('probabilityValue'); | |
| const predictionBadge = document.getElementById('predictionBadge'); | |
| const feedbackAccurate = document.getElementById('feedbackAccurate'); | |
| const feedbackInaccurate = document.getElementById('feedbackInaccurate'); | |
| const refreshTransactionsBtn = document.getElementById('refreshTransactions'); | |
| const exportTransactionsBtn = document.getElementById('exportTransactions'); | |
| const filterStatus = document.getElementById('filterStatus'); | |
| const filterDate = document.getElementById('filterDate'); | |
| const searchTransaction = document.getElementById('searchTransaction'); | |
| const transactionsBody = document.getElementById('transactionsBody'); | |
| const prevPageBtn = document.getElementById('prevPage'); | |
| const nextPageBtn = document.getElementById('nextPage'); | |
| const pageInfo = document.getElementById('pageInfo'); | |
| const tableInfo = document.getElementById('tableInfo'); | |
| // Chart instances | |
| let fraudDistributionChart, locationFraudChart, featureImportanceChart, amountDistributionChart, locationHeatmapChart; | |
| // Initialize the application | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeEventListeners(); | |
| loadDashboardData(); | |
| initializeCharts(); | |
| loadSampleTransactions(); | |
| loadFeatureImportance(); | |
| // Simulate user login | |
| simulateLogin(); | |
| // Populate backend URL input from saved value | |
| const backendUrlInput = document.getElementById('backendUrlInput'); | |
| const setBackendUrlBtn = document.getElementById('setBackendUrlBtn'); | |
| if (backendUrlInput) backendUrlInput.value = localStorage.getItem('API_BASE_URL') || API_BASE_URL; | |
| if (setBackendUrlBtn) { | |
| setBackendUrlBtn.addEventListener('click', function() { | |
| const val = (backendUrlInput && backendUrlInput.value || '').trim(); | |
| if (!val) { | |
| localStorage.removeItem('API_BASE_URL'); | |
| API_BASE_URL = 'http://localhost:5000'; | |
| showNotification('warning', 'Backend URL cleared — using localhost'); | |
| return; | |
| } | |
| try { | |
| new URL(val); | |
| } catch (e) { | |
| showNotification('danger', 'Invalid URL'); | |
| return; | |
| } | |
| localStorage.setItem('API_BASE_URL', val); | |
| API_BASE_URL = val; | |
| showNotification('success', 'Backend URL saved'); | |
| }); | |
| } | |
| // Register service worker for PWA (if available) | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('/sw.js') | |
| .then(reg => console.log('ServiceWorker registered', reg.scope)) | |
| .catch(err => console.warn('ServiceWorker registration failed', err)); | |
| } | |
| }); | |
| function initializeEventListeners() { | |
| // Navigation | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const page = this.getAttribute('data-page'); | |
| switchPage(page); | |
| }); | |
| }); | |
| // Logout | |
| logoutBtn.addEventListener('click', function() { | |
| showNotification('success', 'Logged out successfully'); | |
| setTimeout(() => { | |
| window.location.reload(); | |
| }, 1500); | |
| }); | |
| // Prediction form | |
| predictionForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| predictFraud(); | |
| }); | |
| // Feedback buttons | |
| feedbackAccurate.addEventListener('click', function() { | |
| submitFeedback(true); | |
| this.classList.add('active'); | |
| feedbackInaccurate.classList.remove('active'); | |
| }); | |
| feedbackInaccurate.addEventListener('click', function() { | |
| submitFeedback(false); | |
| this.classList.add('active'); | |
| feedbackAccurate.classList.remove('active'); | |
| }); | |
| // Transactions | |
| refreshTransactionsBtn.addEventListener('click', loadSampleTransactions); | |
| exportTransactionsBtn.addEventListener('click', exportTransactionsToCSV); | |
| filterStatus.addEventListener('change', filterTransactions); | |
| filterDate.addEventListener('change', filterTransactions); | |
| searchTransaction.addEventListener('input', filterTransactions); | |
| prevPageBtn.addEventListener('click', () => changePage(-1)); | |
| nextPageBtn.addEventListener('click', () => changePage(1)); | |
| } | |
| function switchPage(pageName) { | |
| // Hide all pages | |
| Object.values(pages).forEach(page => { | |
| page.classList.remove('active'); | |
| }); | |
| // Show selected page | |
| pages[pageName].classList.add('active'); | |
| // Update active nav link | |
| navLinks.forEach(link => { | |
| link.classList.remove('active'); | |
| if (link.getAttribute('data-page') === pageName) { | |
| link.classList.add('active'); | |
| } | |
| }); | |
| // Load data for specific pages | |
| if (pageName === 'dashboard') { | |
| updateDashboardStats(); | |
| } else if (pageName === 'transactions') { | |
| renderTransactionsTable(); | |
| } | |
| } | |
| // Simulate login - in real app, this would be an API call | |
| function simulateLogin() { | |
| showNotification('success', `Welcome back, ${currentUser.name}!`); | |
| } | |
| // Load dashboard data | |
| function loadDashboardData() { | |
| // In a real app, this would be an API call | |
| updateDashboardStats(); | |
| } | |
| function updateDashboardStats() { | |
| // Simulate API response | |
| const stats = { | |
| totalFraud: Math.floor(Math.random() * 500) + 120, | |
| totalSafe: Math.floor(Math.random() * 10000) + 8500, | |
| totalTransactions: Math.floor(Math.random() * 10500) + 9000, | |
| accuracyRate: (Math.random() * 10 + 90).toFixed(1) + '%' | |
| }; | |
| document.getElementById('totalFraud').textContent = stats.totalFraud; | |
| document.getElementById('totalSafe').textContent = stats.totalSafe; | |
| document.getElementById('totalTransactions').textContent = stats.totalTransactions; | |
| document.getElementById('accuracyRate').textContent = stats.accuracyRate; | |
| } | |
| // Initialize charts | |
| function initializeCharts() { | |
| // Fraud Distribution Chart | |
| const fraudDistributionCtx = document.getElementById('fraudDistributionChart').getContext('2d'); | |
| fraudDistributionChart = new Chart(fraudDistributionCtx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Legitimate', 'Fraudulent'], | |
| datasets: [{ | |
| data: [92, 8], | |
| backgroundColor: [ | |
| 'rgba(39, 174, 96, 0.8)', | |
| 'rgba(231, 76, 60, 0.8)' | |
| ], | |
| borderColor: [ | |
| 'rgba(39, 174, 96, 1)', | |
| 'rgba(231, 76, 60, 1)' | |
| ], | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'bottom' | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `${context.label}: ${context.raw}%`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Location Fraud Chart | |
| const locationFraudCtx = document.getElementById('locationFraudChart').getContext('2d'); | |
| locationFraudChart = new Chart(locationFraudCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland'], | |
| datasets: [ | |
| { | |
| label: 'Fraudulent', | |
| data: [45, 38, 32, 28, 24, 15, 12], | |
| backgroundColor: 'rgba(231, 76, 60, 0.8)', | |
| borderColor: 'rgba(231, 76, 60, 1)', | |
| borderWidth: 1 | |
| }, | |
| { | |
| label: 'Legitimate', | |
| data: [455, 412, 398, 322, 376, 285, 288], | |
| backgroundColor: 'rgba(39, 174, 96, 0.8)', | |
| borderColor: 'rgba(39, 174, 96, 1)', | |
| borderWidth: 1 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| grid: { | |
| display: false | |
| } | |
| }, | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| callback: function(value) { | |
| return value; | |
| } | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top' | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Load feature importance data | |
| function loadFeatureImportance() { | |
| // Feature importance data based on the provided dataset columns | |
| const featureImportanceData = [ | |
| { feature: 'transactionAmount', importance: 0.28 }, | |
| { feature: 'customerDevice', importance: 0.18 }, | |
| { feature: 'location', importance: 0.15 }, | |
| { feature: 'customerEmail', importance: 0.12 }, | |
| { feature: 'No_Transactions', importance: 0.08 }, | |
| { feature: 'merchant', importance: 0.07 }, | |
| { feature: 'category', importance: 0.05 }, | |
| { feature: 'customerIPAddress', importance: 0.04 }, | |
| { feature: 'TransactionType', importance: 0.02 }, | |
| { feature: 'customerBillingAddress', importance: 0.01 } | |
| ]; | |
| // Render feature importance list | |
| const featureList = document.getElementById('featureList'); | |
| featureList.innerHTML = ''; | |
| featureImportanceData.forEach((item, index) => { | |
| const featureItem = document.createElement('div'); | |
| featureItem.className = 'feature-item'; | |
| featureItem.innerHTML = ` | |
| <div class="feature-rank">${index + 1}</div> | |
| <div class="feature-name">${formatFeatureName(item.feature)}</div> | |
| <div class="feature-importance">${(item.importance * 100).toFixed(1)}%</div> | |
| `; | |
| featureList.appendChild(featureItem); | |
| }); | |
| // Create feature importance chart | |
| const featureImportanceCtx = document.getElementById('featureImportanceChart').getContext('2d'); | |
| featureImportanceChart = new Chart(featureImportanceCtx, { | |
| type: 'horizontalBar', | |
| data: { | |
| labels: featureImportanceData.map(item => formatFeatureName(item.feature)), | |
| datasets: [{ | |
| label: 'Importance', | |
| data: featureImportanceData.map(item => item.importance * 100), | |
| backgroundColor: 'rgba(52, 152, 219, 0.8)', | |
| borderColor: 'rgba(52, 152, 219, 1)', | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| indexAxis: 'y', | |
| scales: { | |
| x: { | |
| beginAtZero: true, | |
| ticks: { | |
| callback: function(value) { | |
| return value + '%'; | |
| } | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `Importance: ${context.raw.toFixed(1)}%`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Create amount distribution chart | |
| const amountDistributionCtx = document.getElementById('amountDistributionChart').getContext('2d'); | |
| amountDistributionChart = new Chart(amountDistributionCtx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [ | |
| { | |
| label: 'Legitimate Transactions', | |
| data: generateRandomAmountData(200, 4, 500, false), | |
| backgroundColor: 'rgba(39, 174, 96, 0.6)', | |
| borderColor: 'rgba(39, 174, 96, 1)', | |
| pointRadius: 5 | |
| }, | |
| { | |
| label: 'Fraudulent Transactions', | |
| data: generateRandomAmountData(50, 300, 4200, true), | |
| backgroundColor: 'rgba(231, 76, 60, 0.6)', | |
| borderColor: 'rgba(231, 76, 60, 1)', | |
| pointRadius: 6 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| position: 'bottom', | |
| title: { | |
| display: true, | |
| text: 'Transaction Amount ($)' | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Frequency' | |
| }, | |
| ticks: { | |
| display: false | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top' | |
| } | |
| } | |
| } | |
| }); | |
| // Create location heatmap chart | |
| const locationHeatmapCtx = document.getElementById('locationHeatmapChart').getContext('2d'); | |
| locationHeatmapChart = new Chart(locationHeatmapCtx, { | |
| type: 'radar', | |
| data: { | |
| labels: ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland'], | |
| datasets: [ | |
| { | |
| label: 'Fraud Risk Level', | |
| data: [85, 78, 72, 65, 58, 42, 38], | |
| backgroundColor: 'rgba(231, 76, 60, 0.2)', | |
| borderColor: 'rgba(231, 76, 60, 1)', | |
| pointBackgroundColor: 'rgba(231, 76, 60, 1)', | |
| pointBorderColor: '#fff', | |
| pointHoverBackgroundColor: '#fff', | |
| pointHoverBorderColor: 'rgba(231, 76, 60, 1)' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| r: { | |
| angleLines: { | |
| display: true | |
| }, | |
| suggestedMin: 0, | |
| suggestedMax: 100 | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function formatFeatureName(feature) { | |
| // Format feature names for display | |
| const nameMap = { | |
| 'transactionAmount': 'Transaction Amount', | |
| 'customerDevice': 'Customer Device', | |
| 'location': 'Location', | |
| 'customerEmail': 'Customer Email', | |
| 'No_Transactions': 'Number of Transactions', | |
| 'merchant': 'Merchant', | |
| 'category': 'Category', | |
| 'customerIPAddress': 'Customer IP Address', | |
| 'TransactionType': 'Transaction Type', | |
| 'customerBillingAddress': 'Billing Address' | |
| }; | |
| return nameMap[feature] || feature.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); | |
| } | |
| function generateRandomAmountData(count, min, max, isFraud) { | |
| const data = []; | |
| for (let i = 0; i < count; i++) { | |
| const amount = Math.random() * (max - min) + min; | |
| const y = isFraud ? Math.random() * 0.5 + 0.5 : Math.random() * 0.5; | |
| data.push({ x: amount, y: y }); | |
| } | |
| return data; | |
| } | |
| // Load sample transactions | |
| function loadSampleTransactions() { | |
| // Generate sample transactions based on the dataset | |
| const locations = ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland', 'New Mexico', 'South Dakota', 'Montana']; | |
| const merchants = ['Amazon', 'Walmart', 'Target', 'Best Buy', 'Apple', 'Starbucks', 'Uber', 'Airbnb', 'Netflix', 'Spotify']; | |
| const categories = ['retail', 'travel', 'food', 'entertainment', 'digital']; | |
| transactions = []; | |
| for (let i = 1; i <= 50; i++) { | |
| const amount = (Math.random() * 4000 + 4.3).toFixed(2); | |
| const location = locations[Math.floor(Math.random() * locations.length)]; | |
| const isFraud = Math.random() < 0.08; // 8% chance of fraud | |
| const confidence = (Math.random() * 30 + 70).toFixed(1); // 70-100% confidence | |
| const date = new Date(); | |
| date.setDate(date.getDate() - Math.floor(Math.random() * 30)); | |
| transactions.push({ | |
| id: `TRX${10000 + i}`, | |
| date: date.toISOString(), | |
| amount: parseFloat(amount), | |
| location: location, | |
| merchant: merchants[Math.floor(Math.random() * merchants.length)], | |
| category: categories[Math.floor(Math.random() * categories.length)], | |
| isFraud: isFraud, | |
| confidence: parseFloat(confidence), | |
| status: isFraud ? 'fraud' : 'safe' | |
| }); | |
| } | |
| // Sort by date (newest first) | |
| transactions.sort((a, b) => new Date(b.date) - new Date(a.date)); | |
| renderTransactionsTable(); | |
| showNotification('success', 'Transactions loaded successfully'); | |
| } | |
| function renderTransactionsTable() { | |
| // Filter transactions | |
| let filteredTransactions = [...transactions]; | |
| // Apply status filter | |
| if (filterStatus.value) { | |
| filteredTransactions = filteredTransactions.filter(t => t.status === filterStatus.value); | |
| } | |
| // Apply date filter | |
| if (filterDate.value) { | |
| const filterDateObj = new Date(filterDate.value); | |
| filteredTransactions = filteredTransactions.filter(t => { | |
| const transactionDate = new Date(t.date); | |
| return transactionDate.toDateString() === filterDateObj.toDateString(); | |
| }); | |
| } | |
| // Apply search filter | |
| if (searchTransaction.value) { | |
| const searchTerm = searchTransaction.value.toLowerCase(); | |
| filteredTransactions = filteredTransactions.filter(t => | |
| t.id.toLowerCase().includes(searchTerm) || | |
| t.merchant.toLowerCase().includes(searchTerm) || | |
| t.location.toLowerCase().includes(searchTerm) | |
| ); | |
| } | |
| // Calculate pagination | |
| const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| const startIndex = (currentPage - 1) * transactionsPerPage; | |
| const endIndex = Math.min(startIndex + transactionsPerPage, filteredTransactions.length); | |
| const pageTransactions = filteredTransactions.slice(startIndex, endIndex); | |
| // Update table | |
| transactionsBody.innerHTML = ''; | |
| if (pageTransactions.length === 0) { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td colspan="9" style="text-align: center; padding: 40px; color: #666;"> | |
| <i class="fas fa-search" style="font-size: 2rem; margin-bottom: 15px; display: block;"></i> | |
| No transactions found matching your criteria | |
| </td> | |
| `; | |
| transactionsBody.appendChild(row); | |
| } else { | |
| pageTransactions.forEach(transaction => { | |
| const row = document.createElement('tr'); | |
| const date = new Date(transaction.date); | |
| const formattedDate = date.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| row.innerHTML = ` | |
| <td>${transaction.id}</td> | |
| <td>${formattedDate}</td> | |
| <td>$${transaction.amount.toFixed(2)}</td> | |
| <td>${transaction.location}</td> | |
| <td>${transaction.merchant}</td> | |
| <td>${transaction.category}</td> | |
| <td> | |
| <span class="status-badge ${transaction.isFraud ? 'status-fraud' : 'status-safe'}"> | |
| ${transaction.isFraud ? 'FRAUD' : 'SAFE'} | |
| </span> | |
| </td> | |
| <td>${transaction.confidence}%</td> | |
| <td> | |
| <button class="btn" style="padding: 5px 10px; font-size: 0.85rem;" onclick="viewTransaction('${transaction.id}')"> | |
| <i class="fas fa-eye"></i> View | |
| </button> | |
| </td> | |
| `; | |
| transactionsBody.appendChild(row); | |
| }); | |
| } | |
| // Update pagination controls | |
| prevPageBtn.disabled = currentPage <= 1; | |
| nextPageBtn.disabled = currentPage >= totalPages; | |
| pageInfo.textContent = `Page ${currentPage} of ${totalPages || 1}`; | |
| tableInfo.textContent = `Showing ${startIndex + 1}-${endIndex} of ${filteredTransactions.length} transactions`; | |
| } | |
| function changePage(direction) { | |
| const filteredTransactions = getFilteredTransactions(); | |
| const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage); | |
| currentPage += direction; | |
| if (currentPage < 1) currentPage = 1; | |
| if (currentPage > totalPages) currentPage = totalPages; | |
| renderTransactionsTable(); | |
| } | |
| function getFilteredTransactions() { | |
| let filteredTransactions = [...transactions]; | |
| if (filterStatus.value) { | |
| filteredTransactions = filteredTransactions.filter(t => t.status === filterStatus.value); | |
| } | |
| if (filterDate.value) { | |
| const filterDateObj = new Date(filterDate.value); | |
| filteredTransactions = filteredTransactions.filter(t => { | |
| const transactionDate = new Date(t.date); | |
| return transactionDate.toDateString() === filterDateObj.toDateString(); | |
| }); | |
| } | |
| if (searchTransaction.value) { | |
| const searchTerm = searchTransaction.value.toLowerCase(); | |
| filteredTransactions = filteredTransactions.filter(t => | |
| t.id.toLowerCase().includes(searchTerm) || | |
| t.merchant.toLowerCase().includes(searchTerm) || | |
| t.location.toLowerCase().includes(searchTerm) | |
| ); | |
| } | |
| return filteredTransactions; | |
| } | |
| function filterTransactions() { | |
| currentPage = 1; | |
| renderTransactionsTable(); | |
| } | |
| // Predict fraud - simulate API call to Flask backend | |
| function predictFraud() { | |
| const amount = parseFloat(document.getElementById('transactionAmount').value); | |
| const location = document.getElementById('transactionLocation').value; | |
| const merchant = document.getElementById('merchantName').value || 'Unknown Merchant'; | |
| const category = document.getElementById('transactionCategory').value; | |
| const email = document.getElementById('customerEmail').value; | |
| const device = document.getElementById('customerDevice').value; | |
| const type = document.getElementById('transactionType').value; | |
| // Show loading state | |
| predictButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Analyzing...'; | |
| predictButton.disabled = true; | |
| // Build a payload that matches the preprocessor expected feature names. | |
| // The preprocessor expects a fixed set of features (see preprocessor.feature_names_in_). | |
| // We'll provide best-effort mappings from the form and sensible defaults for missing values. | |
| const now = new Date(); | |
| const payload = { | |
| merchant_name: merchant || '', | |
| avg_amount_per_transaction: amount || 0, | |
| day_of_week: now.getDay(), | |
| amount_deviation_from_location_mean: 0, | |
| transaction_category: category || '', | |
| customer_no_transactions: 0, | |
| customer_lat: null, | |
| transaction_type: type || '', | |
| customer_place_name: null, | |
| merchant_id: null, | |
| location: location || '', | |
| customer_job: null, | |
| age: null, | |
| merchant_long: null, | |
| amount_per_city_pop: 0, | |
| customer_long: null, | |
| distance_customer_merchant: 0, | |
| transactions_per_customer_ratio: 0, | |
| customer_city_population: 0, | |
| merchant_lat: null, | |
| customer_no_payments: 0, | |
| customer_no_orders: 0, | |
| payments_per_order_ratio: 0, | |
| hour_of_day: now.getHours(), | |
| amount: amount || 0, | |
| customer_zip_code: null, | |
| mean_amount_by_location: 0, | |
| fraud_rate_by_location: 0, | |
| customer_gender: null | |
| }; | |
| // Real network call to backend | |
| fetch(`${API_BASE_URL}/predict`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(payload), | |
| }) | |
| .then(async response => { | |
| if (!response.ok) { | |
| // Try to read JSON error if available | |
| let errText = `${response.status} ${response.statusText}`; | |
| try { | |
| const errBody = await response.json(); | |
| if (errBody && errBody.error) errText = errBody.error; | |
| } catch (e) {} | |
| throw new Error(errText); | |
| } | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| // Normalize different shapes (legacy, mock, or production) | |
| displayPredictionResults(data, amount, location, merchant); | |
| }) | |
| .catch(error => { | |
| console.error('Prediction API error:', error); | |
| showNotification('danger', `Prediction service error: ${error.message}`); | |
| // Fallback to local simulation so UI remains usable offline | |
| const isFraud = simulateFraudPrediction(amount, location); | |
| const confidence = (Math.random() * 20 + 80).toFixed(1); | |
| const fraudProbability = isFraud ? (Math.random() * 30 + 70).toFixed(1) : (Math.random() * 20).toFixed(1); | |
| const fallbackResponse = { | |
| prediction: isFraud ? 'fraud' : 'safe', | |
| confidence: parseFloat(confidence), | |
| fraud_probability: parseFloat(fraudProbability) | |
| }; | |
| displayPredictionResults(fallbackResponse, amount, location, merchant); | |
| }) | |
| .finally(() => { | |
| predictButton.innerHTML = '<i class="fas fa-brain"></i> Analyze Transaction for Fraud'; | |
| predictButton.disabled = false; | |
| }); | |
| // Normalize and adapt various API response shapes to UI-friendly values | |
| function normalizePredictionResponse(data) { | |
| // Determine prediction | |
| const isFraud = ( | |
| data.fraud === 1 || | |
| data.fraud_prediction === 1 || | |
| data.prediction === 'fraud' || | |
| data.predicted === 'fraud' || | |
| data.prediction === 1 | |
| ); | |
| // Determine probability (as 0..1) | |
| let probability = null; | |
| if (typeof data.probability === 'number') probability = data.probability; | |
| else if (typeof data.fraud_probability === 'number') probability = (data.fraud_probability > 1 ? data.fraud_probability / 100 : data.fraud_probability); | |
| else if (typeof data.confidence === 'number') probability = (data.confidence > 1 ? data.confidence / 100 : data.confidence); | |
| // Fallback heuristic | |
| if (probability === null) probability = isFraud ? 0.85 : 0.12; | |
| return { | |
| prediction: isFraud ? 'fraud' : 'safe', | |
| probability: Math.max(0, Math.min(1, probability)), | |
| confidence: data.confidence ?? Math.round(probability * 100) | |
| }; | |
| } | |
| function simulateFraudPrediction(amount, location) { | |
| // Simple simulation logic based on dataset patterns | |
| let fraudScore = 0; | |
| // Amount-based risk | |
| if (amount > 3000) fraudScore += 40; | |
| else if (amount > 1000) fraudScore += 20; | |
| else if (amount < 10) fraudScore += 15; | |
| // Location-based risk (from dataset patterns) | |
| const highRiskLocations = ['New York', 'Dallas', 'Phoenix']; | |
| const mediumRiskLocations = ['San Antonio', 'Philadelphia']; | |
| if (highRiskLocations.includes(location)) fraudScore += 35; | |
| else if (mediumRiskLocations.includes(location)) fraudScore += 20; | |
| else if (location === 'Luar Negeri') fraudScore += 50; | |
| // Random factor | |
| fraudScore += Math.random() * 30; | |
| return fraudScore > 60; // Threshold for fraud | |
| } | |
| function displayPredictionResults(data, amount, location, merchant) { | |
| // Update UI with results (support multiple API response shapes) | |
| document.getElementById('resultAmount').textContent = `$${amount.toFixed(2)}`; | |
| document.getElementById('resultLocation').textContent = location; | |
| document.getElementById('resultMerchant').textContent = merchant; | |
| const norm = normalizePredictionResponse(data); | |
| // Update prediction badge | |
| predictionBadge.textContent = norm.prediction === 'fraud' ? 'FRAUD' : 'SAFE'; | |
| predictionBadge.className = `prediction-badge ${norm.prediction === 'fraud' ? 'badge-fraud' : 'badge-safe'}`; | |
| // Update probability bar: use percentage | |
| const pct = Math.round(norm.probability * 100); | |
| probabilityFill.className = `probability-fill ${norm.prediction === 'fraud' ? 'fraud-probability' : 'safe-probability'}`; | |
| probabilityFill.style.width = `${pct}%`; | |
| probabilityValue.textContent = `${pct}%`; | |
| document.getElementById('resultConfidence').textContent = `${norm.confidence}%`; | |
| // Show results container | |
| resultsContainer.style.display = 'block'; | |
| // Reset feedback buttons | |
| feedbackAccurate.classList.remove('active'); | |
| feedbackInaccurate.classList.remove('active'); | |
| // Add to transaction history | |
| const isFraud = norm.prediction === 'fraud'; | |
| const fraudProbability = Math.round(norm.probability * 100); | |
| const newTransaction = { | |
| id: `TRX${10000 + transactions.length + 1}`, | |
| date: new Date().toISOString(), | |
| amount: amount, | |
| location: location, | |
| merchant: merchant, | |
| category: document.getElementById('transactionCategory').value, | |
| isFraud: isFraud, | |
| confidence: norm.confidence, | |
| status: isFraud ? 'fraud' : 'safe' | |
| }; | |
| transactions.unshift(newTransaction); | |
| // Show notification | |
| if (isFraud) { | |
| showNotification('danger', `Fraud detected! Probability: ${fraudProbability}%`, true); | |
| } else { | |
| showNotification('success', `Transaction appears legitimate. Fraud probability: ${fraudProbability}%`); | |
| } | |
| // Scroll to results | |
| resultsContainer.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function submitFeedback(isAccurate) { | |
| // In a real app, this would send feedback to the Flask API | |
| // fetch(`${API_BASE_URL}/feedback`, { | |
| // method: 'POST', | |
| // headers: { | |
| // 'Content-Type': 'application/json', | |
| // }, | |
| // body: JSON.stringify({ | |
| // transaction_id: transactions[0].id, | |
| // prediction_accurate: isAccurate, | |
| // user_id: currentUser.id | |
| // }) | |
| // }) | |
| showNotification('success', `Thank you for your feedback! Model accuracy will be improved.`); | |
| // Reset form after feedback | |
| setTimeout(() => { | |
| predictionForm.reset(); | |
| resultsContainer.style.display = 'none'; | |
| }, 2000); | |
| } | |
| function exportTransactionsToCSV() { | |
| // Create CSV content | |
| const headers = ['ID', 'Date', 'Amount', 'Location', 'Merchant', 'Category', 'Status', 'Confidence']; | |
| const csvContent = [ | |
| headers.join(','), | |
| ...transactions.map(t => [ | |
| t.id, | |
| new Date(t.date).toLocaleDateString(), | |
| t.amount, | |
| t.location, | |
| t.merchant, | |
| t.category, | |
| t.status.toUpperCase(), | |
| t.confidence | |
| ].join(',')) | |
| ].join('\n'); | |
| // Create download link | |
| const blob = new Blob([csvContent], { type: 'text/csv' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `fraud_transactions_${new Date().toISOString().split('T')[0]}.csv`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showNotification('success', 'Transactions exported successfully'); | |
| } | |
| function viewTransaction(transactionId) { | |
| const transaction = transactions.find(t => t.id === transactionId); | |
| if (transaction) { | |
| // Switch to prediction page and populate form | |
| switchPage('predict'); | |
| // Populate form with transaction data | |
| document.getElementById('transactionAmount').value = transaction.amount; | |
| document.getElementById('transactionLocation').value = transaction.location; | |
| document.getElementById('merchantName').value = transaction.merchant; | |
| document.getElementById('transactionCategory').value = transaction.category; | |
| // Trigger prediction | |
| setTimeout(() => { | |
| predictButton.click(); | |
| }, 500); | |
| } | |
| } | |
| // Notification system | |
| function showNotification(type, message, isImportant = false) { | |
| const container = document.getElementById('notificationContainer'); | |
| const notification = document.createElement('div'); | |
| notification.className = `notification ${type}`; | |
| const icons = { | |
| success: 'fa-check-circle', | |
| warning: 'fa-exclamation-triangle', | |
| danger: 'fa-times-circle', | |
| info: 'fa-info-circle' | |
| }; | |
| notification.innerHTML = ` | |
| <div class="notification-icon"> | |
| <i class="fas ${icons[type] || 'fa-info-circle'}"></i> | |
| </div> | |
| <div class="notification-content"> | |
| <h4>${type.charAt(0).toUpperCase() + type.slice(1)}</h4> | |
| <p>${message}</p> | |
| </div> | |
| `; | |
| container.appendChild(notification); | |
| // Trigger animation | |
| setTimeout(() => { | |
| notification.classList.add('show'); | |
| }, 10); | |
| // Auto-remove notification | |
| setTimeout(() => { | |
| notification.classList.remove('show'); | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 500); | |
| }, isImportant ? 8000 : 5000); | |
| } | |
| </script> | |
| </body> | |
| </html> |