Download frontend/src/components/TransactionTable.jsx from WizardCoder2007/aegis-fraud-detector: direct link, hf CLI and curl.
- Browser
- Download file 23.3 kB
-
https://huggingface.co/spaces/WizardCoder2007/aegis-fraud-detector/resolve/main/frontend/src/components/TransactionTable.jsx
- Command line
-
hf download hf://spaces/WizardCoder2007/aegis-fraud-detector/frontend/src/components/TransactionTable.jsx
-
curl -L -o TransactionTable.jsx https://huggingface.co/spaces/WizardCoder2007/aegis-fraud-detector/resolve/main/frontend/src/components/TransactionTable.jsx
23.3 kB
| import React, { useState } from 'react'; | |
| import { | |
| FileText, | |
| Search, | |
| Download, | |
| AlertTriangle, | |
| ShieldAlert, | |
| ShieldCheck, | |
| UserX, | |
| Eye | |
| } from 'lucide-react'; | |
| export default function TransactionTable({ previewData, topSuspicious, muleAccounts, sessionId, backendUrl }) { | |
| const [activeTab, setActiveTab] = useState('preview'); | |
| const [searchTerm, setSearchTerm] = useState(''); | |
| const [riskFilter, setRiskFilter] = useState('All'); | |
| const [currentPage, setCurrentPage] = useState(1); | |
| const [selectedTx, setSelectedTx] = useState(null); // For detail modal | |
| const itemsPerPage = 15; | |
| // Filter & Search preview data | |
| const filteredPreview = (previewData || []).filter(item => { | |
| // Search matching | |
| const searchStr = `${item.F3886 || ''} ${item.F3893 || ''} ${item.F3891 || ''} ${item.Unnamed_0 || item['Unnamed: 0'] || ''}`.toLowerCase(); | |
| const matchesSearch = searchStr.includes(searchTerm.toLowerCase()); | |
| // Risk matching | |
| const matchesRisk = riskFilter === 'All' || item.risk_level === riskFilter; | |
| return matchesSearch && matchesRisk; | |
| }); | |
| // Pagination | |
| const totalPages = Math.ceil(filteredPreview.length / itemsPerPage); | |
| const paginatedData = filteredPreview.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); | |
| const getRiskBadge = (level) => { | |
| switch (level) { | |
| case 'High': | |
| return <span className="badge badge-danger">High Risk</span>; | |
| case 'Medium': | |
| return <span className="badge badge-warning">Medium Risk</span>; | |
| default: | |
| return <span className="badge badge-success">Low Risk</span>; | |
| } | |
| }; | |
| const handleDownload = () => { | |
| if (!sessionId) return; | |
| window.open(`${backendUrl}/api/download-run/${sessionId}`, '_blank'); | |
| }; | |
| return ( | |
| <div className="glass-card animate-fade-in" style={{ marginTop: '24px' }}> | |
| {/* Header and Download */} | |
| <div style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| flexWrap: 'wrap', | |
| gap: '16px', | |
| marginBottom: '20px', | |
| borderBottom: '1px solid var(--border-color)', | |
| paddingBottom: '16px' | |
| }}> | |
| <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> | |
| <FileText style={{ color: 'var(--primary)' }} /> | |
| <h2 style={{ fontSize: '1.25rem', margin: 0 }}>Analysis Logs</h2> | |
| </div> | |
| {sessionId && ( | |
| <button className="btn btn-primary" onClick={handleDownload}> | |
| <Download size={18} /> | |
| Download Annotated CSV | |
| </button> | |
| )} | |
| </div> | |
| {/* Tabs list */} | |
| <div style={{ | |
| display: 'flex', | |
| gap: '12px', | |
| borderBottom: '1px solid var(--border-color)', | |
| marginBottom: '20px', | |
| overflowX: 'auto', | |
| paddingBottom: '2px' | |
| }}> | |
| <button | |
| style={{ | |
| background: 'none', | |
| border: 'none', | |
| color: activeTab === 'preview' ? 'var(--primary)' : 'var(--text-secondary)', | |
| fontWeight: 600, | |
| padding: '10px 16px', | |
| borderBottom: activeTab === 'preview' ? '2px solid var(--primary)' : '2px solid transparent', | |
| cursor: 'pointer', | |
| transition: 'var(--transition)' | |
| }} | |
| onClick={() => { setActiveTab('preview'); setCurrentPage(1); }} | |
| > | |
| Transactions Preview ({filteredPreview.length}) | |
| </button> | |
| <button | |
| style={{ | |
| background: 'none', | |
| border: 'none', | |
| color: activeTab === 'alerts' ? 'var(--primary)' : 'var(--text-secondary)', | |
| fontWeight: 600, | |
| padding: '10px 16px', | |
| borderBottom: activeTab === 'alerts' ? '2px solid var(--primary)' : '2px solid transparent', | |
| cursor: 'pointer', | |
| transition: 'var(--transition)' | |
| }} | |
| onClick={() => setActiveTab('alerts')} | |
| > | |
| Top Risky Alerts ({topSuspicious?.length || 0}) | |
| </button> | |
| <button | |
| style={{ | |
| background: 'none', | |
| border: 'none', | |
| color: activeTab === 'mules' ? 'var(--primary)' : 'var(--text-secondary)', | |
| fontWeight: 600, | |
| padding: '10px 16px', | |
| borderBottom: activeTab === 'mules' ? '2px solid var(--primary)' : '2px solid transparent', | |
| cursor: 'pointer', | |
| transition: 'var(--transition)' | |
| }} | |
| onClick={() => setActiveTab('mules')} | |
| > | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <UserX size={16} /> | |
| Mule Account Candidates ({muleAccounts?.length || 0}) | |
| </div> | |
| </button> | |
| </div> | |
| {/* SEARCH AND FILTERS (Only for preview tab) */} | |
| {activeTab === 'preview' && ( | |
| <div style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| flexWrap: 'wrap', | |
| gap: '16px', | |
| marginBottom: '20px' | |
| }}> | |
| {/* Search box */} | |
| <div style={{ position: 'relative', width: '320px', maxWidth: '100%' }}> | |
| <Search | |
| size={18} | |
| style={{ | |
| position: 'absolute', | |
| left: '12px', | |
| top: '50%', | |
| transform: 'translateY(-50%)', | |
| color: 'var(--text-secondary)' | |
| }} | |
| /> | |
| <input | |
| type="text" | |
| placeholder="Search by account, channel, job..." | |
| className="form-input" | |
| style={{ paddingLeft: '40px' }} | |
| value={searchTerm} | |
| onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }} | |
| /> | |
| </div> | |
| {/* Filter dropdown */} | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> | |
| <span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Risk Filter:</span> | |
| <select | |
| className="form-input" | |
| style={{ width: '150px', padding: '8px 12px' }} | |
| value={riskFilter} | |
| onChange={(e) => { setRiskFilter(e.target.value); setCurrentPage(1); }} | |
| > | |
| <option value="All">All Risk Levels</option> | |
| <option value="High">High Risk</option> | |
| <option value="Medium">Medium Risk</option> | |
| <option value="Low">Low Risk</option> | |
| </select> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 1: ALL PREVIEW */} | |
| {activeTab === 'preview' && ( | |
| <div> | |
| {paginatedData.length === 0 ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}> | |
| No transactions match the selected filters. | |
| </div> | |
| ) : ( | |
| <div style={{ overflowX: 'auto' }}> | |
| <table style={{ | |
| width: '100%', | |
| borderCollapse: 'collapse', | |
| textAlign: 'left', | |
| fontSize: '0.875rem' | |
| }}> | |
| <thead> | |
| <tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}> | |
| <th style={{ padding: '12px' }}>ID</th> | |
| <th style={{ padding: '12px' }}>Date</th> | |
| <th style={{ padding: '12px' }}>Account Type</th> | |
| <th style={{ padding: '12px' }}>Channel</th> | |
| <th style={{ padding: '12px' }}>Job Type</th> | |
| <th style={{ padding: '12px' }}>Risk Prob</th> | |
| <th style={{ padding: '12px' }}>Status</th> | |
| <th style={{ padding: '12px' }}>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {paginatedData.map((row, idx) => { | |
| const rowId = row.Unnamed_0 || row['Unnamed: 0'] || `Row ${idx + 1}`; | |
| return ( | |
| <tr | |
| key={idx} | |
| style={{ | |
| borderBottom: '1px solid var(--border-color)', | |
| background: row.is_suspicious ? 'rgba(239, 68, 68, 0.03)' : 'transparent' | |
| }} | |
| > | |
| <td style={{ padding: '12px', fontWeight: 600 }}>{rowId}</td> | |
| <td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3888 || 'N/A'}</td> | |
| <td style={{ padding: '12px' }}>{row.F3886 || 'N/A'}</td> | |
| <td style={{ padding: '12px' }}>{row.F3893 || 'N/A'}</td> | |
| <td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3891 || 'N/A'}</td> | |
| <td style={{ padding: '12px', fontWeight: 700, color: row.is_suspicious ? 'var(--danger)' : 'var(--text-primary)' }}> | |
| {Math.round(row.fraud_probability * 100)}% | |
| </td> | |
| <td style={{ padding: '12px' }}> | |
| {getRiskBadge(row.risk_level)} | |
| </td> | |
| <td style={{ padding: '12px' }}> | |
| <button | |
| style={{ | |
| background: 'rgba(255,255,255,0.05)', | |
| border: 'none', | |
| borderRadius: '4px', | |
| padding: '4px 8px', | |
| cursor: 'pointer', | |
| color: 'var(--text-secondary)' | |
| }} | |
| onClick={() => setSelectedTx(row)} | |
| title="Inspect Details" | |
| > | |
| <Eye size={14} /> | |
| </button> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {/* Pagination controls */} | |
| {totalPages > 1 && ( | |
| <div style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| marginTop: '20px', | |
| borderTop: '1px solid var(--border-color)', | |
| paddingTop: '16px' | |
| }}> | |
| <span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}> | |
| Page {currentPage} of {totalPages} | |
| </span> | |
| <div style={{ display: 'flex', gap: '8px' }}> | |
| <button | |
| className="btn btn-secondary" | |
| style={{ padding: '6px 12px', fontSize: '0.8rem' }} | |
| disabled={currentPage === 1} | |
| onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} | |
| > | |
| Previous | |
| </button> | |
| <button | |
| className="btn btn-secondary" | |
| style={{ padding: '6px 12px', fontSize: '0.8rem' }} | |
| disabled={currentPage === totalPages} | |
| onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* TAB 2: TOP SUSPICIOUS ALERTS */} | |
| {activeTab === 'alerts' && ( | |
| <div style={{ overflowX: 'auto' }}> | |
| {(!topSuspicious || topSuspicious.length === 0) ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}> | |
| No critical fraud alerts detected. | |
| </div> | |
| ) : ( | |
| <table style={{ | |
| width: '100%', | |
| borderCollapse: 'collapse', | |
| textAlign: 'left', | |
| fontSize: '0.875rem' | |
| }}> | |
| <thead> | |
| <tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}> | |
| <th style={{ padding: '12px' }}>ID</th> | |
| <th style={{ padding: '12px' }}>Risk Probability</th> | |
| <th style={{ padding: '12px' }}>Date</th> | |
| <th style={{ padding: '12px' }}>Account Type</th> | |
| <th style={{ padding: '12px' }}>Channel</th> | |
| <th style={{ padding: '12px' }}>Job Status</th> | |
| <th style={{ padding: '12px' }}>Risk Assessment</th> | |
| <th style={{ padding: '12px' }}>Action</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {topSuspicious.map((row, idx) => { | |
| const rowId = row.Unnamed_0 || row['Unnamed: 0'] || `Alert ${idx + 1}`; | |
| return ( | |
| <tr | |
| key={idx} | |
| style={{ | |
| borderBottom: '1px solid var(--border-color)', | |
| background: 'rgba(239, 68, 68, 0.05)' | |
| }} | |
| > | |
| <td style={{ padding: '12px', fontWeight: 600 }}>{rowId}</td> | |
| <td style={{ padding: '12px', fontWeight: 800, color: 'var(--danger)', display: 'flex', alignItems: 'center', gap: '8px' }}> | |
| <ShieldAlert size={16} /> | |
| {Math.round(row.fraud_probability * 100)}% Risk | |
| </td> | |
| <td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3888 || 'N/A'}</td> | |
| <td style={{ padding: '12px' }}>{row.F3886 || 'N/A'}</td> | |
| <td style={{ padding: '12px' }}>{row.F3893 || 'N/A'}</td> | |
| <td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3891 || 'N/A'}</td> | |
| <td style={{ padding: '12px' }}> | |
| <span className="badge badge-danger">High Threat</span> | |
| </td> | |
| <td style={{ padding: '12px' }}> | |
| <button | |
| className="btn btn-secondary" | |
| style={{ padding: '4px 8px', fontSize: '0.75rem' }} | |
| onClick={() => setSelectedTx(row)} | |
| > | |
| Inspect | |
| </button> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| )} | |
| </div> | |
| )} | |
| {/* TAB 3: MULE ACCOUNT CANDIDATES */} | |
| {activeTab === 'mules' && ( | |
| <div> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem', marginBottom: '16px' }}> | |
| Mule accounts typically act as intermediaries to circulate stolen funds. The table below lists account IDs that have multiple high-risk/suspicious transactions in this file run. | |
| </p> | |
| <div style={{ overflowX: 'auto' }}> | |
| {(!muleAccounts || muleAccounts.length === 0) ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}> | |
| No repeating account-level suspicious transactions detected. | |
| </div> | |
| ) : ( | |
| <table style={{ | |
| width: '100%', | |
| borderCollapse: 'collapse', | |
| textAlign: 'left', | |
| fontSize: '0.875rem' | |
| }}> | |
| <thead> | |
| <tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}> | |
| <th style={{ padding: '12px' }}>Account ID / Value</th> | |
| <th style={{ padding: '12px' }}>Total Transactions</th> | |
| <th style={{ padding: '12px' }}>Suspicious Flagged Count</th> | |
| <th style={{ padding: '12px' }}>Max Probability</th> | |
| <th style={{ padding: '12px' }}>Avg Risk Probability</th> | |
| <th style={{ padding: '12px' }}>Mule Account Threat Rating</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {muleAccounts.map((mule, idx) => { | |
| const avgRisk = Math.round(mule.avg_prob * 100); | |
| const threatScore = mule.suspicious_count * avgRisk; | |
| const isSevere = threatScore > 100 || mule.suspicious_count >= 3; | |
| return ( | |
| <tr | |
| key={idx} | |
| style={{ | |
| borderBottom: '1px solid var(--border-color)', | |
| background: isSevere ? 'rgba(239, 68, 68, 0.04)' : 'transparent' | |
| }} | |
| > | |
| <td style={{ padding: '12px', fontWeight: 600 }}>{mule.F3887 || mule.F3895 || `Acc #${idx}`}</td> | |
| <td style={{ padding: '12px' }}>{mule.tx_count}</td> | |
| <td style={{ padding: '12px', fontWeight: 700, color: 'var(--danger)' }}>{mule.suspicious_count}</td> | |
| <td style={{ padding: '12px' }}>{Math.round(mule.max_prob * 100)}%</td> | |
| <td style={{ padding: '12px', fontWeight: 600 }}>{avgRisk}%</td> | |
| <td style={{ padding: '12px' }}> | |
| {isSevere ? ( | |
| <span className="badge badge-danger" style={{ animation: 'pulse-glow 2s infinite' }}> | |
| CRITICAL MULE | |
| </span> | |
| ) : ( | |
| <span className="badge badge-warning">SUSPECT MULE</span> | |
| )} | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* DETAILS MODAL */} | |
| {selectedTx && ( | |
| <div style={{ | |
| position: 'fixed', | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
| backdropFilter: 'blur(8px)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| zIndex: 1000, | |
| padding: '20px' | |
| }}> | |
| <div className="glass-card animate-fade-in" style={{ | |
| width: '600px', | |
| maxWidth: '100%', | |
| maxHeight: '90vh', | |
| overflowY: 'auto', | |
| background: '#151e33', | |
| border: '1px solid rgba(255,255,255,0.15)', | |
| boxShadow: '0 10px 40px rgba(0,0,0,0.6)' | |
| }}> | |
| <h3 style={{ fontSize: '1.25rem', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}> | |
| <AlertTriangle style={{ color: selectedTx.is_suspicious ? 'var(--danger)' : 'var(--success)' }} /> | |
| Transaction Details & Analysis | |
| </h3> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '20px', background: 'rgba(0,0,0,0.2)', padding: '16px', borderRadius: '8px' }}> | |
| <div> | |
| <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Transaction ID</p> | |
| <p style={{ fontWeight: 600 }}>{selectedTx.Unnamed_0 || selectedTx['Unnamed: 0'] || 'N/A'}</p> | |
| </div> | |
| <div> | |
| <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Risk Assessment</p> | |
| <p>{getRiskBadge(selectedTx.risk_level)}</p> | |
| </div> | |
| <div> | |
| <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Fraud Probability</p> | |
| <p style={{ fontWeight: 700, fontSize: '1.1rem', color: selectedTx.is_suspicious ? 'var(--danger)' : 'var(--success)' }}> | |
| {Math.round(selectedTx.fraud_probability * 100)}% | |
| </p> | |
| </div> | |
| <div> | |
| <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Transaction DateTime</p> | |
| <p>{selectedTx.F3888 || 'N/A'}</p> | |
| </div> | |
| </div> | |
| <h4 style={{ fontSize: '0.95rem', marginBottom: '8px', color: 'var(--text-secondary)' }}>Key Parameters Loaded</h4> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', fontSize: '0.875rem', marginBottom: '24px' }}> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Account Type (F3886):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3886 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Channel (F3893):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3893 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Job Status (F3891):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3891 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Gender (F3892):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3892 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Account Code (F3887):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3887 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Customer Code (F3889):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3889 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Account State (F3890):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3890 || 'N/A'}</span> | |
| </div> | |
| <div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}> | |
| <span style={{ color: 'var(--text-secondary)' }}>Tx Interval (F3894):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3894 || 'N/A'}</span> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}> | |
| <button className="btn btn-secondary" onClick={() => setSelectedTx(null)}> | |
| Close Inspection | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |