Bankbot / frontend /src /app /security /page.tsx
mohsin-devs's picture
fix: remove duplicate SecurityPage component breaking HF Docker build
526fdb9
Raw
History Blame Contribute Delete
17.2 kB
"use client";
import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Shield, AlertTriangle, CheckCircle, RefreshCw,
TrendingUp, Clock, Zap, Eye, XCircle, Activity,
Sparkles, ChevronDown, ChevronUp, Loader2,
} from "lucide-react";
import { aiApi } from "@/lib/api";
import { useAuthStore } from "@/lib/stores/authStore";
interface FraudAlert {
id: string;
fraud_log_id?: string;
risk_score: number;
details: string;
status?: string;
transaction_id?: string;
merchant?: string;
amount?: number;
timestamp?: string;
}
interface FraudData {
total_alerts: number;
pending_reviews: number;
alerts: FraudAlert[];
}
interface FraudExplanation {
ai_explanation: string;
raw_reasons: string[];
amount_vs_avg: number;
user_avg_amount: number;
}
function Skeleton({ className }: { className?: string }) {
return <div className={`shimmer rounded-lg ${className}`} />;
}
function RiskBadge({ score }: { score: number }) {
const level =
score >= 70 ? { label: "High Risk", color: "text-red-400", bg: "bg-red-500/10 border-red-500/20" }
: score >= 40 ? { label: "Suspicious", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }
: { label: "Low Risk", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" };
return (
<span className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${level.color} ${level.bg}`}>
{level.label}
</span>
);
}
function RiskBar({ score }: { score: number }) {
const color = score >= 70 ? "#ef4444" : score >= 40 ? "#f59e0b" : "#10b981";
return (
<div className="flex items-center gap-2">
<div className="h-1.5 w-24 rounded-full bg-white/10 overflow-hidden">
<motion.div initial={{ width: 0 }} animate={{ width: `${score}%` }} transition={{ duration: 0.8, ease: "easeOut" }} className="h-full rounded-full" style={{ background: color }} />
</div>
<span className="text-xs font-mono" style={{ color }}>{score}</span>
</div>
);
}
function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
const [expanded, setExpanded] = useState(false);
const [explanation, setExplanation] = useState<FraudExplanation | null>(null);
const [loadingExpl, setLoadingExpl] = useState(false);
const loadExplanation = async () => {
const logId = alert.fraud_log_id || alert.id;
if (!logId || explanation) { setExpanded(!expanded); return; }
setExpanded(true);
setLoadingExpl(true);
try {
const res = await aiApi.fraudExplain(logId);
setExplanation(res);
} catch {
setExplanation({
ai_explanation: alert.details || "Suspicious activity detected based on transaction patterns.",
raw_reasons: typeof alert.details === "string" ? alert.details.split("; ") : [],
amount_vs_avg: 0,
user_avg_amount: 0,
});
} finally {
setLoadingExpl(false);
}
};
return (
<motion.div initial={{ opacity: 0, x: -8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.04 }}
className={`border-b border-white/5 last:border-0 ${alert.risk_score >= 70 ? "bg-red-500/2" : ""}`}>
<div className="flex items-center gap-4 px-6 py-4 hover:bg-white/[0.02] transition-colors cursor-pointer" onClick={loadExplanation}>
<div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border ${alert.risk_score >= 70 ? "bg-red-500/10 border-red-500/20" : alert.risk_score >= 40 ? "bg-amber-500/10 border-amber-500/20" : "bg-emerald-500/10 border-emerald-500/20"}`}>
{alert.risk_score >= 70 ? <XCircle className="h-5 w-5 text-red-400" /> : alert.risk_score >= 40 ? <AlertTriangle className="h-5 w-5 text-amber-400" /> : <CheckCircle className="h-5 w-5 text-emerald-400" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="text-sm font-medium text-white truncate">{alert.merchant || `Transaction ${(alert.transaction_id || alert.id).slice(0, 8)}...`}</p>
<RiskBadge score={alert.risk_score} />
{alert.status && <span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-zinc-400 capitalize">{alert.status}</span>}
</div>
<p className="text-xs text-zinc-500 mt-0.5 truncate">
{typeof alert.details === "string" ? alert.details : Array.isArray(alert.details) ? (alert.details as string[]).join(" · ") : "Suspicious activity detected"}
</p>
</div>
<div className="flex-shrink-0 hidden sm:block"><RiskBar score={alert.risk_score} /></div>
{alert.amount != null && <div className="flex-shrink-0 text-right"><p className="text-sm font-semibold text-white">${Math.abs(alert.amount).toFixed(2)}</p></div>}
<div className="flex-shrink-0 flex items-center gap-1 text-zinc-500">
<Sparkles className="h-3.5 w-3.5 text-cyan-400" />
{expanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
</div>
</div>
<AnimatePresence>
{expanded && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25 }} className="overflow-hidden">
<div className="mx-6 mb-4 rounded-xl border border-cyan-500/20 bg-cyan-500/5 p-4">
<div className="flex items-start gap-2">
<Sparkles className="h-4 w-4 text-cyan-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-xs font-semibold text-cyan-400 mb-2">AI Fraud Explanation</p>
{loadingExpl ? (
<div className="flex items-center gap-2">
<Loader2 className="h-3.5 w-3.5 animate-spin text-cyan-400" />
<span className="text-xs text-zinc-400">Analyzing transaction patterns...</span>
</div>
) : explanation ? (
<div className="space-y-3">
<p className="text-sm text-zinc-200 leading-relaxed">{explanation.ai_explanation}</p>
{explanation.raw_reasons.length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-zinc-400">Detection signals:</p>
{explanation.raw_reasons.map((r, i) => (
<div key={i} className="flex items-start gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-amber-400 flex-shrink-0 mt-1.5" />
<p className="text-xs text-zinc-400">{r}</p>
</div>
))}
</div>
)}
{explanation.amount_vs_avg > 0 && (
<div className="flex items-center gap-4 pt-1 border-t border-white/8">
<div><p className="text-[10px] text-zinc-500">This transaction</p><p className="text-sm font-bold text-white">${alert.amount?.toFixed(2)}</p></div>
<div className="text-zinc-600">vs</div>
<div><p className="text-[10px] text-zinc-500">Your average</p><p className="text-sm font-bold text-zinc-300">${explanation.user_avg_amount.toFixed(2)}</p></div>
<div className="ml-auto"><span className="rounded-full bg-red-500/15 border border-red-500/30 px-2 py-0.5 text-xs font-bold text-red-400">{explanation.amount_vs_avg.toFixed(1)}× average</span></div>
</div>
)}
</div>
) : null}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.07 } } };
const itemVariants = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } };
export default function SecurityPage() {
const { user } = useAuthStore();
const [data, setData] = useState<FraudData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<"all" | "high" | "pending">("all");
const load = useCallback(async () => {
setLoading(true); setError(null);
try {
const res = await aiApi.fraudAnalysis(user?.user_id);
const raw = res as unknown as Record<string, unknown>;
const alerts: FraudAlert[] = (raw.alerts as FraudAlert[]) || [];
setData({ total_alerts: (raw.total_alerts as number) ?? alerts.length, pending_reviews: (raw.pending_reviews as number) ?? alerts.filter((a) => a.status === "pending").length, alerts });
} catch (err) { setError((err as Error).message); }
finally { setLoading(false); }
}, [user?.user_id]);
useEffect(() => { load(); }, [load]);
const alerts = data?.alerts || [];
const filtered = alerts.filter((a) => filter === "high" ? a.risk_score >= 70 : filter === "pending" ? a.status === "pending" || !a.status : true);
const highRiskCount = alerts.filter((a) => a.risk_score >= 70).length;
const suspiciousCount = alerts.filter((a) => a.risk_score >= 40 && a.risk_score < 70).length;
const safeCount = alerts.filter((a) => a.risk_score < 40).length;
const avgScore = alerts.length ? Math.round(alerts.reduce((s, a) => s + a.risk_score, 0) / alerts.length) : 0;
return (
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="flex flex-col gap-6">
<motion.div variants={itemVariants} className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">Security Center</h1>
<p className="text-zinc-400 mt-1 text-sm">AI-powered fraud detection · Click any alert for a full AI explanation</p>
</div>
<button onClick={load} className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
<RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />Refresh
</button>
</motion.div>
{error && (
<motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-amber-500/20 bg-amber-500/5 px-5 py-3">
<AlertTriangle className="h-4 w-4 text-amber-400 flex-shrink-0" />
<p className="text-xs text-zinc-400"><span className="text-amber-400 font-medium">Could not load fraud data</span> — {error}</p>
</motion.div>
)}
<motion.div variants={itemVariants} className="flex items-center gap-4 rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 via-cyan-500/5 to-blue-500/10 px-5 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex-shrink-0"><Shield className="h-5 w-5 text-emerald-400" /></div>
<div className="flex-1">
<p className="text-sm font-semibold text-white">AI Fraud Shield Active</p>
<p className="text-xs text-zinc-400 mt-0.5">Click any flagged transaction to get a full AI explanation of exactly why it was flagged.</p>
</div>
<div className="flex items-center gap-1.5"><div className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" /><span className="text-xs text-emerald-400 font-medium">Live</span></div>
</motion.div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{[
{ label: "Total Alerts", value: loading ? "—" : String(data?.total_alerts ?? 0), icon: Activity, color: "text-blue-400", border: "border-blue-500/20", bg: "from-blue-500/10 to-blue-500/5" },
{ label: "High Risk", value: loading ? "—" : String(highRiskCount), icon: XCircle, color: "text-red-400", border: "border-red-500/20", bg: "from-red-500/10 to-red-500/5" },
{ label: "Suspicious", value: loading ? "—" : String(suspiciousCount), icon: AlertTriangle, color: "text-amber-400", border: "border-amber-500/20", bg: "from-amber-500/10 to-amber-500/5" },
{ label: "Avg Risk Score", value: loading ? "—" : `${avgScore}/100`, icon: TrendingUp, color: "text-purple-400", border: "border-purple-500/20", bg: "from-purple-500/10 to-purple-500/5" },
].map((stat) => (
<motion.div key={stat.label} variants={itemVariants} className={`rounded-2xl border ${stat.border} bg-gradient-to-br ${stat.bg} p-5`}>
<div className="flex items-start justify-between">
<div><p className="text-xs text-zinc-400 uppercase tracking-wider">{stat.label}</p><p className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</p></div>
<stat.icon className={`h-5 w-5 ${stat.color} opacity-60`} />
</div>
</motion.div>
))}
</div>
<motion.div variants={itemVariants} className="glass-card p-6">
<h2 className="text-base font-semibold text-white mb-4">Active Detection Rules</h2>
<div className="grid gap-3 sm:grid-cols-2">
{[
{ icon: TrendingUp, label: "Amount Spike Detection", desc: "Flags transactions 3.5× above your average" },
{ icon: Clock, label: "Late-Night Monitoring", desc: "Alerts on activity between 11PM – 4AM" },
{ icon: Zap, label: "Rapid Transaction Guard", desc: "Detects multiple transactions within 3 minutes" },
{ icon: Eye, label: "Duplicate Payment Check", desc: "Catches same merchant + amount within 10 minutes" },
].map((rule) => (
<div key={rule.label} className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0"><rule.icon className="h-4 w-4 text-emerald-400" /></div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-white">{rule.label}</p>
<div className="flex items-center gap-1 flex-shrink-0"><div className="h-1.5 w-1.5 rounded-full bg-emerald-400" /><span className="text-[10px] text-emerald-400">ON</span></div>
</div>
<p className="text-xs text-zinc-500 mt-0.5">{rule.desc}</p>
</div>
</div>
))}
</div>
</motion.div>
<motion.div variants={itemVariants} className="glass-card overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
<div>
<h2 className="text-base font-semibold text-white">Fraud Alerts</h2>
<p className="text-xs text-zinc-400 mt-0.5">{loading ? "Loading..." : `${filtered.length} alert${filtered.length !== 1 ? "s" : ""} · Click any row for AI explanation`}</p>
</div>
<div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
{(["all", "high", "pending"] as const).map((f) => (
<button key={f} onClick={() => setFilter(f)} className={`rounded-lg px-3 py-1.5 text-xs font-medium capitalize transition-all ${filter === f ? "bg-white/10 text-white" : "text-zinc-500 hover:text-zinc-300"}`}>
{f === "all" ? "All" : f === "high" ? "High Risk" : "Pending"}
</button>
))}
</div>
</div>
{loading ? (
<div className="space-y-px p-4">{[1,2,3].map((i) => <Skeleton key={i} className="h-16 w-full rounded-xl" />)}</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center gap-3 py-16 text-center">
<CheckCircle className="h-12 w-12 text-emerald-500/40" />
<p className="text-sm font-medium text-zinc-400">{alerts.length === 0 ? "No fraud alerts detected" : "No alerts match this filter"}</p>
<p className="text-xs text-zinc-600">{alerts.length === 0 ? "All transactions look clean. AI Shield is monitoring in real-time." : "Try a different filter."}</p>
</div>
) : (
<div>{filtered.map((alert, i) => <AlertRow key={alert.id} alert={alert} index={i} />)}</div>
)}
</motion.div>
{!loading && safeCount > 0 && (
<motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 px-5 py-3">
<CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
<p className="text-xs text-zinc-400">
<span className="text-emerald-400 font-medium">{safeCount} transaction{safeCount !== 1 ? "s" : ""} verified safe</span>{" "}— risk score below threshold.
</p>
</motion.div>
)}
</motion.div>
);
}