FinMK / frontend /src /components /AnomalyFeed.jsx
Kumar
Refactor: Exclude PDF and CSV files from Git to fix HF push error
24e6f5b
import { useState, useEffect } from 'react';
import { AlertTriangle, ShieldCheck, Zap } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import api from '../api/axios';
import { useSettings } from '../context/SettingsContext';
const AnomalyFeed = () => {
const { currencySymbol } = useSettings();
const [anomalies, setAnomalies] = useState([]);
const [loading, setLoading] = useState(true);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
const fetchAnomalies = async () => {
try {
const response = await api.get('analytics/anomalies/');
setAnomalies(response.data);
} catch (err) {
console.error("Anomaly Error:", err);
} finally {
setLoading(false);
}
};
fetchAnomalies();
}, []);
// Prepare chart data - Recharts Scatter needs numeric X axis often for best results with type="number"
// converting date string to timestamp
const chartData = anomalies.map(a => ({
...a,
x: new Date(a.date).getTime(),
y: a.amount
}));
const displayedAnomalies = showAll ? anomalies : anomalies.slice(0, 6);
if (loading) return (
<div className="glass-panel" style={{ height: '100%', minHeight: '300px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div className="spinner"></div>
</div>
);
return (
<div className="glass-panel" style={{
height: '550px', // Increased from 450px
minHeight: '250px',
padding: '0',
overflow: 'hidden',
display: 'flex', flexDirection: 'column',
border: '1px solid rgba(239, 68, 68, 0.2)',
boxShadow: '0 0 20px rgba(239, 68, 68, 0.05)'
}}>
<div style={{
padding: '1rem',
borderBottom: '1px solid var(--glass-border)',
background: 'rgba(239, 68, 68, 0.05)',
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<h3 style={{ margin: 0, fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#f87171' }}>
<ShieldCheck size={18} /> Security Radar
</h3>
<span style={{
fontSize: '0.7rem', fontWeight: 'bold',
background: anomalies.length ? '#ef4444' : '#10b981',
color: 'white', padding: '0.1rem 0.5rem', borderRadius: '99px'
}}>
{anomalies.length}
</span>
</div>
{/* Anomaly Visualization Chart */}
{anomalies.length > 0 && (
<div style={{ height: '150px', width: '100%', borderBottom: '1px solid var(--glass-border)', background: 'linear-gradient(to bottom, rgba(0,0,0,0.2), transparent)' }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={anomalies} margin={{ top: 20, right: 20, bottom: 20, left: 0 }}>
<XAxis
dataKey="date"
tick={false}
axisLine={false}
/>
<YAxis
hide
/>
<Tooltip
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
contentStyle={{ backgroundColor: 'rgba(30, 41, 59, 0.95)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' }}
formatter={(value) => [`${currencySymbol}${value}`, 'Amount']}
/>
<Bar dataKey="amount" radius={[4, 4, 0, 0]}>
{anomalies.map((entry, index) => (
<Cell key={`cell-${index}`} fill={`rgba(239, 68, 68, ${(entry.score || 0.5) + 0.4})`} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '0.75rem' }}>
{anomalies.length === 0 ? (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: 'var(--text-muted)', opacity: 0.7 }}>
<ShieldCheck size={32} style={{ marginBottom: '0.5rem', color: '#10b981' }} />
<p style={{ fontSize: '0.9rem' }}>No anomalies detected.</p>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem' }}>
{displayedAnomalies.map((item, idx) => (
<div key={idx} style={{
background: 'rgba(255,255,255,0.03)',
padding: '0.6rem', // Reduced from 0.75rem
borderRadius: '0.5rem',
borderTop: '3px solid #f87171', // Top border for cards
display: 'flex', flexDirection: 'column',
transition: 'all 0.2s',
minHeight: '90px' // Reduced from 100px
}} className="hover:bg-white/5">
<div style={{ marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.3rem', fontSize: '0.8rem', fontWeight: '600', color: '#fff', overflow: 'hidden' }}>
<AlertTriangle size={12} color="#f87171" style={{ flexShrink: 0 }} />
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</span>
</div>
<div style={{ fontSize: '1rem', fontWeight: 'bold', color: '#fff', marginBottom: '0.25rem' }}>
{currencySymbol}{item.amount.toLocaleString()}
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: 'auto' }}>
{item.date}
</div>
<div style={{ fontSize: '0.65rem', marginTop: '0.5rem', color: '#f87171', background: 'rgba(248, 113, 113, 0.1)', padding: '2px 6px', borderRadius: '4px', textAlign: 'center', alignSelf: 'flex-start' }}>
{item.score ? `Risk: ${(item.score * 100).toFixed(0)}%` : 'Detected'}
</div>
</div>
))}
{/* Show More Button - Spans all columns */}
{anomalies.length > 6 && (
<button
onClick={() => setShowAll(!showAll)}
style={{
gridColumn: '1 / -1',
padding: '0.5rem',
marginTop: '0.25rem',
background: 'rgba(255,255,255,0.05)',
border: 'none',
borderRadius: '0.5rem',
color: 'var(--text-muted)',
fontSize: '0.8rem',
cursor: 'pointer',
transition: 'background 0.2s'
}}
className="hover:bg-white/10"
>
{showAll ? 'Show Less' : `Show ${anomalies.length - 6} More`}
</button>
)}
</div>
)}
</div>
<div style={{
padding: '0.5rem', borderTop: '1px solid var(--glass-border)',
fontSize: '0.65rem', color: 'var(--text-muted)', textAlign: 'center',
background: 'rgba(0,0,0,0.2)'
}}>
<Zap size={10} style={{ marginRight: '0.25rem', display: 'inline' }} />
AI Anomaly Detector
</div>
</div>
);
};
export default AnomalyFeed;