WizardCoder2007's picture
changes in frontend
6b63300
Raw
History Blame Contribute Delete
20.5 kB
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'); // 'csv' or 'manual'
// Check backend health
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();
// Poll health every 10 seconds
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);
}
};
// Submit manual prediction
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>
) : (
/* MANUAL CHECK WORKSPACE */
<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>
);
}