| import React, { useState, useEffect } from 'react'; |
| import { |
| ShieldAlert, |
| UploadCloud, |
| RefreshCw, |
| Settings, |
| CheckCircle, |
| Activity, |
| FileSpreadsheet, |
| AlertTriangle, |
| Zap |
| } from 'lucide-react'; |
| import DashboardCharts from './components/DashboardCharts'; |
| import TransactionTable from './components/TransactionTable'; |
| import ManualQueryForm from './components/ManualQueryForm'; |
|
|
| const BACKEND_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' |
| ? 'http://127.0.0.1:8000' |
| : window.location.origin; |
|
|
| export default function App() { |
| const [modelName, setModelName] = useState('voting'); |
| const [threshold, setThreshold] = useState(0.30); |
| const [file, setFile] = useState(null); |
| const [dragActive, setDragActive] = useState(false); |
| const [processing, setProcessing] = useState(false); |
| const [stats, setStats] = useState(null); |
| const [lastUsedConfig, setLastUsedConfig] = useState(null); |
| const [backendHealth, setBackendHealth] = useState({ connected: false, loading: true }); |
| const [activeTab, setActiveTab] = useState('csv'); |
|
|
| |
| const checkHealth = async () => { |
| try { |
| const res = await fetch(`${BACKEND_URL}/api/health`); |
| const data = await res.json(); |
| if (data.status === 'healthy') { |
| setBackendHealth({ connected: true, loading: false }); |
| } else { |
| setBackendHealth({ connected: false, loading: false }); |
| } |
| } catch (err) { |
| setBackendHealth({ connected: false, loading: false }); |
| } |
| }; |
|
|
| useEffect(() => { |
| checkHealth(); |
| |
| const interval = setInterval(checkHealth, 10000); |
| return () => clearInterval(interval); |
| }, []); |
|
|
| const handleDrag = (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| if (e.type === "dragenter" || e.type === "dragover") { |
| setDragActive(true); |
| } else if (e.type === "dragleave") { |
| setDragActive(false); |
| } |
| }; |
|
|
| const handleDrop = (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| setDragActive(false); |
| |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { |
| const droppedFile = e.dataTransfer.files[0]; |
| if (droppedFile.name.endsWith('.csv')) { |
| setFile(droppedFile); |
| } else { |
| alert("Only CSV files are supported!"); |
| } |
| } |
| }; |
|
|
| const handleFileChange = (e) => { |
| if (e.target.files && e.target.files[0]) { |
| setFile(e.target.files[0]); |
| } |
| }; |
|
|
| const handleUpload = async () => { |
| if (!file) return; |
| setProcessing(true); |
| setStats(null); |
|
|
| const formData = new FormData(); |
| formData.append("file", file); |
| formData.append("model_name", modelName); |
| formData.append("threshold", threshold); |
|
|
| try { |
| const response = await fetch(`${BACKEND_URL}/api/predict-csv`, { |
| method: "POST", |
| body: formData, |
| }); |
|
|
| if (!response.ok) { |
| let errorMsg = "Upload analysis failed"; |
| try { |
| const errorData = await response.json(); |
| errorMsg = errorData.detail || errorMsg; |
| } catch (_) { |
| try { |
| errorMsg = await response.text(); |
| } catch (_) {} |
| } |
| throw new Error(errorMsg); |
| } |
|
|
| const data = await response.json(); |
| setStats(data); |
| setLastUsedConfig({ modelName, threshold }); |
| } catch (err) { |
| console.error(err); |
| alert(`Error running analysis: ${err.message}`); |
| } finally { |
| setProcessing(false); |
| } |
| }; |
|
|
| |
| const handleManualPredict = async (dataRow) => { |
| const response = await fetch(`${BACKEND_URL}/api/predict-single`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ |
| data: dataRow, |
| model_name: modelName, |
| threshold: threshold |
| }), |
| }); |
|
|
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(errorData.detail || "Query failed"); |
| } |
|
|
| return await response.json(); |
| }; |
|
|
| const configChanged = stats && lastUsedConfig && |
| (lastUsedConfig.modelName !== modelName || lastUsedConfig.threshold !== threshold); |
|
|
| return ( |
| <div className="dashboard-container"> |
| |
| {/* 1. HEADER BRANDING */} |
| <header style={{ |
| display: 'flex', |
| justifyContent: 'space-between', |
| alignItems: 'center', |
| borderBottom: '1px solid var(--border-color)', |
| paddingBottom: '16px', |
| flexWrap: 'wrap', |
| gap: '16px' |
| }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> |
| <div style={{ |
| background: 'linear-gradient(135deg, #ef4444, #3b82f6)', |
| padding: '10px', |
| borderRadius: '10px', |
| boxShadow: '0 0 15px rgba(239, 68, 68, 0.25)', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center' |
| }}> |
| <ShieldAlert size={28} style={{ color: 'white' }} /> |
| </div> |
| <div> |
| <h1 style={{ fontSize: '1.5rem', fontWeight: 800, letterSpacing: '0.05em', background: 'linear-gradient(to right, #f8fafc, #94a3b8)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}> |
| FraudShield AI |
| </h1> |
| <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>Suspicious Transactions & Mule Account Detection</p> |
| </div> |
| </div> |
| |
| {/* Backend health status badge */} |
| <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
| <Activity size={16} style={{ color: backendHealth.connected ? 'var(--success)' : 'var(--danger)' }} /> |
| <span style={{ fontSize: '0.875rem', fontWeight: 600 }}> |
| Backend: {backendHealth.loading ? ( |
| <span style={{ color: 'var(--text-muted)' }}>Connecting...</span> |
| ) : backendHealth.connected ? ( |
| <span style={{ color: 'var(--success)' }}>ONLINE</span> |
| ) : ( |
| <span style={{ color: 'var(--danger)' }}>OFFLINE</span> |
| )} |
| </span> |
| </div> |
| </div> |
| </header> |
| |
| {/* 2. PARAMETERS CONTROLLER BAR */} |
| <section className="glass-card" style={{ |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| flexWrap: 'wrap', |
| gap: '24px', |
| padding: '16px 24px' |
| }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> |
| <Settings size={18} style={{ color: 'var(--text-secondary)' }} /> |
| <h2 style={{ fontSize: '1rem', margin: 0, fontWeight: 500 }}>Global ML Config</h2> |
| </div> |
| |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px', alignItems: 'center' }}> |
| {/* Model Selector */} |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
| <span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Active Model:</span> |
| <select |
| className="form-input" |
| style={{ width: '180px', padding: '8px 12px' }} |
| value={modelName} |
| onChange={(e) => setModelName(e.target.value)} |
| > |
| <option value="voting">Voting Classifier</option> |
| <option value="xgboost">XGBoost Classifier</option> |
| <option value="random_forest">Random Forest</option> |
| </select> |
| </div> |
| |
| {/* Threshold Slider */} |
| <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> |
| <span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Alert Sensitivity:</span> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
| <input |
| type="range" |
| min="0.10" |
| max="0.80" |
| step="0.02" |
| style={{ |
| width: '120px', |
| accentColor: 'var(--primary)', |
| cursor: 'pointer' |
| }} |
| value={threshold} |
| onChange={(e) => setThreshold(parseFloat(e.target.value))} |
| /> |
| <span style={{ |
| fontFamily: 'monospace', |
| fontSize: '0.875rem', |
| fontWeight: 600, |
| color: 'var(--primary)', |
| background: 'rgba(59, 130, 246, 0.15)', |
| padding: '2px 6px', |
| borderRadius: '4px' |
| }}> |
| {threshold.toFixed(2)} |
| </span> |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| {/* 3. APP NAVIGATION TABS */} |
| <div style={{ display: 'flex', gap: '16px' }}> |
| <button |
| className={`btn ${activeTab === 'csv' ? 'btn-primary' : 'btn-secondary'}`} |
| onClick={() => setActiveTab('csv')} |
| > |
| <FileSpreadsheet size={18} /> |
| Batch CSV Mode |
| </button> |
| <button |
| className={`btn ${activeTab === 'manual' ? 'btn-primary' : 'btn-secondary'}`} |
| onClick={() => setActiveTab('manual')} |
| > |
| <Zap size={18} /> |
| Single Transaction Inspector |
| </button> |
| </div> |
| |
| {/* 4. WORKSPACES */} |
| {activeTab === 'csv' ? ( |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}> |
| |
| {configChanged && ( |
| <div className="glass-card animate-fade-in" style={{ |
| background: 'rgba(245, 158, 11, 0.08)', |
| border: '1px solid rgba(245, 158, 11, 0.25)', |
| color: 'var(--warning)', |
| padding: '12px 16px', |
| display: 'flex', |
| alignItems: 'center', |
| gap: '12px', |
| fontSize: '0.9rem', |
| borderRadius: '8px' |
| }}> |
| <AlertTriangle size={18} /> |
| <span> |
| <strong>Configuration modified:</strong> Model settings have changed. Please click <strong>Execute Analysis</strong> to apply these settings to the active dataset. |
| </span> |
| </div> |
| )} |
| |
| {/* Drag & Drop Upload Container */} |
| <div |
| className="glass-card" |
| onDragEnter={handleDrag} |
| onDragOver={handleDrag} |
| onDragLeave={handleDrag} |
| onDrop={handleDrop} |
| style={{ |
| border: dragActive ? '2px dashed var(--primary)' : '1px dashed var(--border-color)', |
| background: dragActive ? 'rgba(59, 130, 246, 0.04)' : 'var(--bg-card)', |
| textAlign: 'center', |
| padding: '40px 20px', |
| cursor: 'pointer', |
| display: 'flex', |
| flexDirection: 'column', |
| alignItems: 'center', |
| justifyContent: 'center', |
| gap: '16px', |
| transition: 'var(--transition)' |
| }} |
| > |
| <input |
| type="file" |
| id="csv-file-upload" |
| style={{ display: 'none' }} |
| accept=".csv" |
| onChange={handleFileChange} |
| /> |
| |
| <label htmlFor="csv-file-upload" style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px' }}> |
| <UploadCloud size={48} style={{ color: dragActive ? 'var(--primary)' : 'var(--text-secondary)', transition: 'var(--transition)' }} /> |
| <div> |
| <p style={{ fontWeight: 600, fontSize: '1.1rem', marginBottom: '4px' }}> |
| {file ? file.name : "Drag and drop your transaction CSV here"} |
| </p> |
| <p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}> |
| {file ? `${(file.size / (1024 * 1024)).toFixed(2)} MB` : "or click here to browse files"} |
| </p> |
| </div> |
| </label> |
| |
| {file && ( |
| <div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}> |
| <button |
| className="btn btn-secondary" |
| onClick={() => { setFile(null); setStats(null); }} |
| disabled={processing} |
| > |
| Clear File |
| </button> |
| <button |
| className="btn btn-primary" |
| onClick={handleUpload} |
| disabled={processing || !backendHealth.connected} |
| > |
| {processing ? ( |
| <> |
| <RefreshCw size={18} style={{ animation: 'spin 1.5s linear infinite' }} /> |
| Analyzing Records... |
| </> |
| ) : ( |
| "Execute Analysis" |
| )} |
| </button> |
| </div> |
| )} |
| </div> |
| |
| {/* LOADING SCREEN */} |
| {processing && ( |
| <div className="glass-card animate-fade-in" style={{ textAlign: 'center', padding: '60px' }}> |
| <RefreshCw size={40} style={{ animation: 'spin 2s linear infinite', color: 'var(--primary)', marginBottom: '16px' }} /> |
| <h3 style={{ fontSize: '1.2rem', marginBottom: '8px' }}>Processing Financial Transaction Logs</h3> |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', maxWidth: '480px', margin: '0 auto' }}> |
| Applying Robust Imputation, Vectorized Min-Max Scaling, and ensemble predictions. Please wait... |
| </p> |
| </div> |
| )} |
| |
| {/* CSV METRICS & RESULTS PANELS */} |
| {stats && ( |
| <div className="animate-fade-in" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}> |
| |
| {/* Metrics summary row */} |
| <div style={{ |
| display: 'grid', |
| gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', |
| gap: '24px' |
| }}> |
| {/* Metric 1 */} |
| <div className="glass-card" style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '20px' }}> |
| <div style={{ padding: '12px', borderRadius: '10px', background: 'rgba(59, 130, 246, 0.1)', color: 'var(--primary)' }}> |
| <FileSpreadsheet size={24} /> |
| </div> |
| <div> |
| <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Transactions Analyzed</p> |
| <p style={{ fontSize: '1.6rem', fontWeight: 700 }}>{stats.total_count.toLocaleString()}</p> |
| </div> |
| </div> |
| |
| {/* Metric 2 */} |
| <div className="glass-card" style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '20px', borderLeft: '3px solid var(--danger)' }}> |
| <div style={{ padding: '12px', borderRadius: '10px', background: 'var(--danger-glow)', color: 'var(--danger)' }}> |
| <ShieldAlert size={24} /> |
| </div> |
| <div> |
| <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Suspicious Flagged</p> |
| <p style={{ fontSize: '1.6rem', fontWeight: 700, color: 'var(--danger)' }}>{stats.suspicious_count.toLocaleString()}</p> |
| </div> |
| </div> |
| |
| {/* Metric 3 */} |
| <div className="glass-card" style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '20px' }}> |
| <div style={{ padding: '12px', borderRadius: '10px', background: 'rgba(245, 158, 11, 0.1)', color: 'var(--warning)' }}> |
| <AlertTriangle size={24} /> |
| </div> |
| <div> |
| <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Alert Rate</p> |
| <p style={{ fontSize: '1.6rem', fontWeight: 700, color: 'var(--warning)' }}>{Math.round(stats.suspicious_rate * 1000) / 10}%</p> |
| </div> |
| </div> |
| |
| {/* Metric 4 */} |
| <div className="glass-card" style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '20px' }}> |
| <div style={{ padding: '12px', borderRadius: '10px', background: 'rgba(16, 185, 129, 0.1)', color: 'var(--success)' }}> |
| <CheckCircle size={24} /> |
| </div> |
| <div> |
| <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Mule Account Leads</p> |
| <p style={{ fontSize: '1.6rem', fontWeight: 700, color: 'var(--success)' }}>{stats.mule_accounts?.length || 0}</p> |
| </div> |
| </div> |
| </div> |
| |
| {/* Recharts widgets */} |
| <DashboardCharts stats={stats} /> |
| |
| {/* Transactions logs table */} |
| <TransactionTable |
| previewData={stats.preview} |
| topSuspicious={stats.top_suspicious} |
| muleAccounts={stats.mule_accounts} |
| sessionId={stats.session_id} |
| backendUrl={BACKEND_URL} |
| /> |
| |
| </div> |
| )} |
| |
| </div> |
| ) : ( |
| |
| <div style={{ |
| display: 'grid', |
| gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', |
| gap: '24px', |
| alignItems: 'stretch' |
| }}> |
| <div> |
| <ManualQueryForm |
| onSubmit={handleManualPredict} |
| modelName={modelName} |
| threshold={threshold} |
| /> |
| </div> |
| |
| <div className="glass-card" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> |
| <h3 style={{ fontSize: '1.1rem', display: 'flex', alignItems: 'center', gap: '8px' }}> |
| <ShieldAlert style={{ color: 'var(--primary)' }} /> |
| Suspicious Indicators Explanation |
| </h3> |
| <p style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', lineHeight: '1.5' }}> |
| Our machine learning models evaluate suspicious proceeds based on several key operational rules derived from the training dataset: |
| </p> |
| |
| <ul style={{ |
| fontSize: '0.875rem', |
| color: 'var(--text-secondary)', |
| lineHeight: '1.6', |
| paddingLeft: '20px', |
| display: 'flex', |
| flexDirection: 'column', |
| gap: '12px' |
| }}> |
| <li> |
| <strong>Transaction Interval (F3894):</strong> High frequency or consecutive rapid transfers (e.g., intervals under 5 seconds) suggest automated routing scripts often used by mule systems. |
| </li> |
| <li> |
| <strong>Target Account Profiling (F3886 & F3891):</strong> Accounts owned by students, unemployed individuals, or newly opened profiles receiving high-volume deposits. |
| </li> |
| <li> |
| <strong>Channel Threat Mapping (F3893):</strong> Rapid internet/mobile-based channels showing sudden variance compared to retail/branch histories. |
| </li> |
| <li> |
| <strong>Limit-matching amounts (F3895):</strong> Transactions structured just below mandatory reporting limits (e.g. daily account limits) to avoid immediate audit. |
| </li> |
| </ul> |
| |
| <div style={{ |
| marginTop: 'auto', |
| padding: '12px 16px', |
| background: 'rgba(59, 130, 246, 0.05)', |
| border: '1px solid rgba(59, 130, 246, 0.1)', |
| borderRadius: '8px', |
| fontSize: '0.8rem' |
| }}> |
| 💡 <strong>Tip:</strong> Toggle between the <em>Safe Preset</em> and the <em>Suspicious Preset</em> in the inspector to test how the threshold affects the risk assessments. |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|