ReconAI / index.html
ACA050's picture
Update index.html
eb7ff2e verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Financial Reconciliation Engine 🧠</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
colors: {
brand: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
},
surface: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
}
}
}
}
}
</script>
<style>
* { scrollbar-width: thin; scrollbar-color: #475569 transparent; }
*::-webkit-scrollbar { width: 6px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 8px rgba(99,102,241,0.4); }
50% { box-shadow: 0 0 20px rgba(99,102,241,0.8); }
}
@keyframes slide-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
@keyframes count-up { from { opacity: 0.5; } to { opacity: 1; } }
.animate-slide-in { animation: slide-in 0.4s ease-out forwards; }
.animate-fade-in { animation: fade-in 0.3s ease-out forwards; }
.animate-pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
.animate-float { animation: float 3s ease-in-out infinite; }
.shimmer-bg {
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.glass-card {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(16px);
border: 1px solid rgba(99, 102, 241, 0.15);
}
.glass-card-light {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(99, 102, 241, 0.1);
}
.risk-critical { border-left: 4px solid #ef4444; }
.risk-high { border-left: 4px solid #f97316; }
.risk-medium { border-left: 4px solid #eab308; }
.risk-low { border-left: 4px solid #22c55e; }
.tab-active {
background: linear-gradient(135deg, #4f46e5, #6366f1);
color: white;
box-shadow: 0 4px 12px rgba(99,102,241,0.4);
}
.network-node { transition: all 0.3s ease; cursor: pointer; }
.network-node:hover { filter: brightness(1.3); transform: scale(1.1); }
.data-table tr { transition: background-color 0.15s ease; }
.progress-bar {
background: linear-gradient(90deg, #4f46e5, #818cf8, #4f46e5);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
border-radius: 9999px 9999px 0 0;
}
.stat-card-purple::before { background: linear-gradient(90deg, #6366f1, #a78bfa); }
.stat-card-green::before { background: linear-gradient(90deg, #22c55e, #4ade80); }
.stat-card-orange::before { background: linear-gradient(90deg, #f97316, #fb923c); }
.stat-card-red::before { background: linear-gradient(90deg, #ef4444, #f87171); }
.stat-card-cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
.stat-card-pink::before { background: linear-gradient(90deg, #ec4899, #f472b6); }
.glow-text {
text-shadow: 0 0 20px rgba(99,102,241,0.5);
}
.sidebar-link {
transition: all 0.2s ease;
position: relative;
}
.sidebar-link::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: #6366f1;
border-radius: 0 4px 4px 0;
transform: scaleY(0);
transition: transform 0.2s ease;
}
.sidebar-link.active::before,
.sidebar-link:hover::before {
transform: scaleY(1);
}
.sidebar-link.active {
background: rgba(99, 102, 241, 0.15);
color: #818cf8;
}
.tooltip-container { position: relative; }
.tooltip-container .tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(4px);
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
z-index: 50;
}
.tooltip-container:hover .tooltip {
opacity: 1;
transform: translateX(-50%) translateY(-4px);
}
</style>
</head>
<body class="bg-surface-950 text-surface-100 font-sans min-h-screen">
<!-- Background Effects -->
<div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-brand-600/10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-80 h-80 bg-purple-600/8 rounded-full blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 w-64 h-64 bg-cyan-600/5 rounded-full blur-3xl"></div>
</div>
<div class="flex min-h-screen relative z-10">
<!-- Sidebar -->
<aside id="sidebar" class="w-64 border-r border-surface-800/50 bg-surface-950/80 backdrop-blur-xl flex flex-col transition-all duration-300 fixed lg:relative z-40 -translate-x-full lg:translate-x-0 h-screen">
<!-- Logo -->
<div class="p-5 border-b border-surface-800/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center animate-pulse-glow">
<i data-lucide="brain" class="w-5 h-5 text-white"></i>
</div>
<div>
<h1 class="text-sm font-bold text-white tracking-tight">ReconAI</h1>
<p class="text-[10px] text-surface-400 font-mono uppercase tracking-widest">Financial Engine</p>
</div>
</div>
</div>
<!-- Nav Links -->
<nav class="flex-1 p-3 space-y-1 overflow-y-auto">
<p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2">Main</p>
<button onclick="switchTab('dashboard')" class="sidebar-link active w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="dashboard">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
</button>
<button onclick="switchTab('reconciliation')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="reconciliation">
<i data-lucide="git-merge" class="w-4 h-4"></i> Reconciliation
</button>
<button onclick="switchTab('anomaly')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="anomaly">
<i data-lucide="alert-triangle" class="w-4 h-4"></i> Anomaly Detection
</button>
<p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2 mt-4">Analysis</p>
<button onclick="switchTab('fraud')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="fraud">
<i data-lucide="network" class="w-4 h-4"></i> Fraud Network
</button>
<button onclick="switchTab('ai-explain')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="ai-explain">
<i data-lucide="message-square-text" class="w-4 h-4"></i> ReconAI
</button>
<button onclick="switchTab('vector')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="vector">
<i data-lucide="database" class="w-4 h-4"></i> Vector Memory
</button>
</nav>
<!-- Status -->
<div class="p-4 border-t border-surface-800/50">
<div class="glass-card rounded-lg p-3">
<div class="flex items-center gap-2 mb-2">
<span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
<span class="text-xs text-surface-300">Engine Online</span>
</div>
<div class="flex items-center gap-2 text-[10px] text-surface-500">
<span>FAISS: Active</span>
<span></span>
<span>LLM: Connected</span>
</div>
</div>
</div>
</aside>
<!-- Overlay for mobile sidebar -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden lg:hidden" onclick="toggleSidebar()"></div>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-h-screen overflow-x-hidden">
<!-- Top Bar -->
<header class="sticky top-0 z-20 border-b border-surface-800/50 bg-surface-950/80 backdrop-blur-xl">
<div class="flex items-center justify-between px-4 lg:px-6 py-3">
<div class="flex items-center gap-3">
<button onclick="toggleSidebar()" class="lg:hidden p-2 rounded-lg hover:bg-surface-800 transition">
<i data-lucide="menu" class="w-5 h-5"></i>
</button>
<div>
<h2 id="page-title" class="text-lg font-semibold text-white">Dashboard</h2>
<p id="page-subtitle" class="text-xs text-surface-400">Financial reconciliation overview</p>
</div>
</div>
<div class="flex items-center gap-2">
<div class="hidden sm:flex items-center gap-2 bg-surface-900 border border-surface-700/50 rounded-lg px-3 py-2">
<i data-lucide="search" class="w-4 h-4 text-surface-400"></i>
<input type="text" placeholder="Search transactions..." class="bg-transparent text-sm text-surface-200 placeholder-surface-500 outline-none w-40 lg:w-56">
</div>
<input type="file" id="books-file" class="hidden" accept=".csv" onchange="updateFileLabel('books')">
<input type="file" id="gst-file" class="hidden" accept=".csv" onchange="updateFileLabel('gst')">
<button onclick="document.getElementById('books-file').click()" id="btn-books" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
<i data-lucide="upload-cloud" class="w-4 h-4"></i>
<span class="hidden sm:inline" id="lbl-books">Books CSV</span>
</button>
<button onclick="document.getElementById('gst-file').click()" id="btn-gst" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
<i data-lucide="upload-cloud" class="w-4 h-4"></i>
<span class="hidden sm:inline" id="lbl-gst">GST CSV</span>
</button>
<button onclick="runReconciliation()" id="btn-run" class="flex items-center gap-2 bg-gradient-to-r from-brand-600 to-purple-600 hover:from-brand-500 hover:to-purple-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-all shadow-lg shadow-brand-600/25">
<i data-lucide="play" class="w-4 h-4"></i>
<span class="hidden sm:inline">Run Engine</span>
</button>
<button onclick="exportCSV()" class="p-2 rounded-lg hover:bg-surface-800 transition tooltip-container relative">
<i data-lucide="download" class="w-4 h-4 text-surface-400"></i>
<div class="tooltip bg-surface-800 text-xs px-2 py-1 rounded whitespace-nowrap">Export CSV</div>
</button>
</div>
</div>
</header>
<!-- Content Area -->
<div class="flex-1 p-4 lg:p-6 space-y-6">
<!-- ==================== DASHBOARD TAB ==================== -->
<section id="tab-dashboard" class="tab-content space-y-6">
<!-- Stat Cards Row -->
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 lg:gap-4">
<div class="stat-card stat-card-purple glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:0ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Total Records</span>
<div class="w-8 h-8 rounded-lg bg-brand-600/20 flex items-center justify-center">
<i data-lucide="file-text" class="w-4 h-4 text-brand-400"></i>
</div>
</div>
<p id="stat-total-records" class="text-2xl font-bold text-white">0</p>
<p id="stat-total-records-sub" class="text-[10px] text-green-400 mt-1 flex items-center gap-1">Awaiting data</p>
</div>
<div class="stat-card stat-card-green glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:80ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Matched</span>
<div class="w-8 h-8 rounded-lg bg-green-600/20 flex items-center justify-center">
<i data-lucide="check-circle" class="w-4 h-4 text-green-400"></i>
</div>
</div>
<p id="stat-matched" class="text-2xl font-bold text-white">0</p>
<p id="stat-matched-sub" class="text-[10px] text-green-400 mt-1">Awaiting data</p>
</div>
<div class="stat-card stat-card-orange glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:160ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Unmatched</span>
<div class="w-8 h-8 rounded-lg bg-orange-600/20 flex items-center justify-center">
<i data-lucide="x-circle" class="w-4 h-4 text-orange-400"></i>
</div>
</div>
<p id="stat-unmatched" class="text-2xl font-bold text-white">0</p>
<p id="stat-unmatched-sub" class="text-[10px] text-orange-400 mt-1">Awaiting data</p>
</div>
<div class="stat-card stat-card-red glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:240ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Anomalies</span>
<div class="w-8 h-8 rounded-lg bg-red-600/20 flex items-center justify-center">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
</div>
</div>
<p id="stat-anomalies" class="text-2xl font-bold text-white">0</p>
<p id="stat-anomalies-sub" class="text-[10px] text-red-400 mt-1">Awaiting data</p>
</div>
<div class="stat-card stat-card-cyan glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:320ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Fraud Rings</span>
<div class="w-8 h-8 rounded-lg bg-cyan-600/20 flex items-center justify-center">
<i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
</div>
</div>
<p id="stat-fraud-rings" class="text-2xl font-bold text-white">0</p>
<p id="stat-fraud-rings-sub" class="text-[10px] text-cyan-400 mt-1">Awaiting data</p>
</div>
<div class="stat-card stat-card-pink glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:400ms">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-surface-400 font-medium">Risk Score</span>
<div class="w-8 h-8 rounded-lg bg-pink-600/20 flex items-center justify-center">
<i data-lucide="gauge" class="w-4 h-4 text-pink-400"></i>
</div>
</div>
<p id="stat-risk-score" class="text-2xl font-bold text-white">0.0<span class="text-sm text-surface-400">/10</span></p>
<p id="stat-risk-score-sub" class="text-[10px] text-pink-400 mt-1">Awaiting data</p>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
<div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:100ms">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-white">Reconciliation Trend</h3>
</div>
<div class="relative w-full h-[250px]">
<canvas id="chart-recon-trend" class="w-full"></canvas>
</div>
</div>
<div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:200ms">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-white">Anomaly Distribution</h3>
</div>
<div class="relative w-full h-[250px]">
<canvas id="chart-anomaly-dist" class="w-full"></canvas>
</div>
</div>
</div>
<!-- Match Confidence + Recent Alerts -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
<div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:150ms">
<h3 class="font-semibold text-white mb-4">Match Confidence</h3>
<div class="relative w-full h-[250px]">
<canvas id="chart-confidence"></canvas>
</div>
</div>
<div class="lg:col-span-2 glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:250ms">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-white">Recent Alerts</h3>
<span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-medium"><span id="alerts-count">0</span> active</span>
</div>
<div id="recent-alerts" class="space-y-3 max-h-[280px] overflow-y-auto pr-2">
<p class="text-sm text-surface-400">No alerts yet.</p>
</div>
</div>
</div>
</section>
<!-- ==================== RECONCILIATION TAB ==================== -->
<section id="tab-reconciliation" class="tab-content hidden space-y-6">
<!-- Recon Stats -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="glass-card rounded-xl p-4 flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-green-600/20 flex items-center justify-center">
<i data-lucide="check-circle-2" class="w-6 h-6 text-green-400"></i>
</div>
<div>
<p id="recon-stat-exact" class="text-2xl font-bold text-white">0</p>
<p class="text-xs text-surface-400">Exact Matches</p>
</div>
</div>
<div class="glass-card rounded-xl p-4 flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-brand-600/20 flex items-center justify-center">
<i data-lucide="fuzzy" class="w-6 h-6 text-brand-400"></i>
</div>
<div>
<p id="recon-stat-fuzzy" class="text-2xl font-bold text-white">0</p>
<p class="text-xs text-surface-400">Fuzzy Matches</p>
</div>
</div>
<div class="glass-card rounded-xl p-4 flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-purple-600/20 flex items-center justify-center">
<i data-lucide="sparkles" class="w-6 h-6 text-purple-400"></i>
</div>
<div>
<p id="recon-stat-semantic" class="text-2xl font-bold text-white">0</p>
<p class="text-xs text-surface-400">AI Semantic</p>
</div>
</div>
<div class="glass-card rounded-xl p-4 flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-red-600/20 flex items-center justify-center">
<i data-lucide="x-circle" class="w-6 h-6 text-red-400"></i>
</div>
<div>
<p id="recon-stat-unmatched" class="text-2xl font-bold text-white">0</p>
<p class="text-xs text-surface-400">Unmatched</p>
</div>
</div>
</div>
<!-- Matching Table -->
<div class="glass-card rounded-xl overflow-hidden">
<div class="flex items-center justify-between p-4 border-b border-surface-800/50">
<h3 class="font-semibold text-white">Reconciliation Results</h3>
<div class="flex items-center gap-2">
<select id="match-filter" onchange="filterReconTable()" class="bg-surface-800/50 border border-surface-700/50 rounded-lg text-xs px-3 py-1.5 text-surface-300 outline-none">
<option value="all">All Results</option>
<option value="matched">Matched</option>
<option value="fuzzy">Fuzzy Match</option>
<option value="unmatched">Unmatched</option>
</select>
<button class="flex items-center gap-1.5 bg-brand-600/20 text-brand-300 text-xs font-medium px-3 py-1.5 rounded-lg hover:bg-brand-600/30 transition">
<i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm data-table">
<thead>
<tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
<th class="px-4 py-3 text-left font-semibold">Books Entry</th>
<th class="px-4 py-3 text-left font-semibold">GST Entry</th>
<th class="px-4 py-3 text-left font-semibold">Amount</th>
<th class="px-4 py-3 text-left font-semibold">Match Type</th>
<th class="px-4 py-3 text-left font-semibold">Confidence</th>
<th class="px-4 py-3 text-left font-semibold">Status</th>
</tr>
</thead>
<tbody id="recon-table-body">
</tbody>
</table>
</div>
<div class="flex items-center justify-between p-4 border-t border-surface-800/50">
<p class="text-xs text-surface-400">Showing <span id="showing-count" id="showing-count">--</span> of <span id="total-count" id="total-count">--</span> results</p>
<div class="flex items-center gap-1">
<button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Prev</button>
<button class="px-3 py-1 rounded-md bg-brand-600 text-white text-xs">1</button>
<button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">2</button>
<button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">3</button>
<button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Next</button>
</div>
</div>
</div>
</section>
<!-- ==================== ANOMALY TAB ==================== -->
<section id="tab-anomaly" class="tab-content hidden space-y-6">
<!-- Anomaly Stats -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="glass-card rounded-xl p-5">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-red-600/20 flex items-center justify-center">
<i data-lucide="shield-alert" class="w-5 h-5 text-red-400"></i>
</div>
<div>
<p class="text-lg font-bold text-white">Isolation Forest</p>
<p class="text-[10px] text-surface-400 font-mono">contamination=0.05</p>
</div>
</div>
<p id="anomaly-trained-sub" class="text-xs text-surface-400 mb-2">Anomaly detection model pending execution.</p>
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 bg-green-500/20 text-green-400 text-[10px] font-semibold rounded-full">ACTIVE</span>
<span class="px-2 py-0.5 bg-brand-500/20 text-brand-400 text-[10px] font-semibold rounded-full">v2.4</span>
</div>
</div>
<div class="glass-card rounded-xl p-5">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-orange-600/20 flex items-center justify-center">
<i data-lucide="radar" class="w-5 h-5 text-orange-400"></i>
</div>
<div>
<p class="text-lg font-bold text-white">Score Range</p>
<p id="anomaly-score-range" class="text-[10px] text-surface-400 font-mono">--</p>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between text-xs">
<span class="text-surface-400">Critical (> 0.3)</span>
<span id="anomaly-crit-count" class="text-red-400 font-mono">0</span>
</div>
<div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-crit-bar" class="bg-red-500 h-1.5 rounded-full" style="width:0%"></div></div>
<div class="flex items-center justify-between text-xs">
<span class="text-surface-400">High (0.1 to 0.3)</span>
<span id="anomaly-high-count" class="text-orange-400 font-mono">0</span>
</div>
<div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-high-bar" class="bg-orange-500 h-1.5 rounded-full" style="width:0%"></div></div>
<div class="flex items-center justify-between text-xs">
<span class="text-surface-400">Medium (< 0.1)</span>
<span id="anomaly-med-count" class="text-yellow-400 font-mono">0</span>
</div>
<div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-med-bar" class="bg-yellow-500 h-1.5 rounded-full" style="width:0%"></div></div>
</div>
</div>
<div class="glass-card rounded-xl p-5">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
<i data-lucide="brain" class="w-5 h-5 text-brand-400"></i>
</div>
<div>
<p class="text-lg font-bold text-white">AI LLM Analysis</p>
<p class="text-[10px] text-surface-400 font-mono">ReconAI</p>
</div>
</div>
<p class="text-xs text-surface-400 mb-3">Natural language explanations generated for each anomaly.</p>
<p class="text-xs text-brand-300 bg-brand-900/20 p-2 rounded border border-brand-500/20">Click "View" on any flagged transaction below to generate a real-time audit explanation.</p>
</div>
</div>
<!-- Anomaly Table -->
<div class="glass-card rounded-xl overflow-hidden">
<div class="flex items-center justify-between p-4 border-b border-surface-800/50">
<h3 class="font-semibold text-white flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
Flagged Transactions
</h3>
<div class="flex items-center gap-2">
<button class="text-xs px-3 py-1.5 rounded-lg bg-red-600/20 text-red-400 hover:bg-red-600/30 transition font-medium">Critical (<span id="filter-crit">0</span>)</button>
<button class="text-xs px-3 py-1.5 rounded-lg bg-surface-800/50 text-surface-300 hover:bg-surface-700/50 transition">All (<span id="filter-all">0</span>)</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm data-table">
<thead>
<tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
<th class="px-4 py-3 text-left font-semibold">Invoice</th>
<th class="px-4 py-3 text-left font-semibold">Vendor</th>
<th class="px-4 py-3 text-left font-semibold">Amount</th>
<th class="px-4 py-3 text-left font-semibold">Anomaly Score</th>
<th class="px-4 py-3 text-left font-semibold">Risk Level</th>
<th class="px-4 py-3 text-left font-semibold">AI Explanation</th>
</tr>
</thead>
<tbody id="anomaly-table-body">
</tbody>
</table>
</div>
</div>
</section>
<!-- ==================== FRAUD NETWORK TAB ==================== -->
<section id="tab-fraud" class="tab-content hidden space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Network Graph -->
<div class="lg:col-span-2 glass-card rounded-xl overflow-hidden">
<div class="flex items-center justify-between p-4 border-b border-surface-800/50">
<h3 class="font-semibold text-white flex items-center gap-2">
<i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
Circular Trading Network
</h3>
<div class="flex items-center gap-2">
</div>
</div>
<div class="relative">
<canvas id="fraud-canvas" width="800" height="500" class="w-full bg-surface-950/50"></canvas>
<div class="absolute top-3 left-3 bg-surface-900/80 backdrop-blur-sm border border-surface-700/30 rounded-lg p-3 text-xs space-y-2">
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-red-500"></span> Critical Node</div>
<div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-surface-500"></span> Linked</div>
</div>
</div>
</div>
<!-- Fraud Details -->
<div class="space-y-4">
<div class="glass-card rounded-xl p-5">
<h3 class="font-semibold text-white mb-3 flex items-center gap-2">
<i data-lucide="shield-alert" class="w-4 h-4 text-red-400"></i>
Detected Rings
</h3>
<div class="space-y-3" id="fraud-rings-list">
<p class="text-sm text-surface-400">No fraud rings detected yet. Run Engine to analyze.</p>
</div>
</div>
<div class="glass-card rounded-xl p-5">
<h3 class="font-semibold text-white mb-3 flex items-center gap-2">
<i data-lucide="bar-chart-3" class="w-4 h-4 text-brand-400"></i>
Network Stats
</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-xs text-surface-400">Total Nodes</span>
<span id="fraud-nodes" class="text-sm font-mono text-white">0</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-surface-400">Total Edges</span>
<span id="fraud-edges" class="text-sm font-mono text-white">0</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ==================== AI EXPLANATIONS TAB ==================== -->
<section id="tab-ai-explain" class="tab-content hidden space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- LLM Queries -->
<div class="space-y-4">
<div class="glass-card rounded-xl p-5">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
<i data-lucide="message-square-text" class="w-4 h-4 text-brand-400"></i>
Ask the AI Auditor
</h3>
<div class="space-y-3 mb-4">
<div id="chat-messages" class="space-y-3 max-h-[400px] overflow-y-auto pr-1">
<div class="flex gap-3">
<div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
<i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
</div>
<div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
<p class="text-sm text-surface-200">Welcome! I'm your AI audit assistant powered by ReconAI. Ask me about any discrepancy, anomaly, or vendor relationship and I'll provide a detailed explanation.</p>
<p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<input id="chat-input" type="text" placeholder="Ask about a discrepancy..." class="flex-1 bg-surface-900/50 border border-surface-700/50 rounded-lg px-3 py-2 text-sm text-white placeholder-surface-500 outline-none focus:border-brand-500/50 focus:ring-1 focus:ring-brand-500/25 transition" onkeydown="if(event.key==='Enter')sendChatMessage()">
<button onclick="sendChatMessage()" class="p-2 bg-brand-600 hover:bg-brand-500 rounded-lg transition">
<i data-lucide="send" class="w-4 h-4 text-white"></i>
</button>
</div>
<div class="flex items-center gap-2 mt-3 flex-wrap">
<button onclick="askSuggested('Explain the highest risk anomaly detected in this batch')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Explain highest risk anomaly</button>
<button onclick="askSuggested('Summarize the top reasons for discrepancies')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Summarize discrepancies</button>
</div>
</div>
</div>
<!-- ReconAI List -->
<div class="glass-card rounded-xl p-5">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
<i data-lucide="sparkles" class="w-4 h-4 text-purple-400"></i>
Generated Explanations
</h3>
<div class="space-y-3 max-h-[530px] overflow-y-auto pr-1" id="explanations-list">
</div>
</div>
</div>
</section>
<!-- ==================== VECTOR MEMORY TAB ==================== -->
<section id="tab-vector" class="tab-content hidden space-y-6">
<div class="flex justify-center">
<div class="glass-card rounded-xl p-5 w-full max-w-md">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
<i data-lucide="database" class="w-5 h-5 text-brand-400"></i>
</div>
<div>
<p class="font-semibold text-white">FAISS Index</p>
<p class="text-[10px] text-surface-400 font-mono">L2 • 384-dim • FlatL2</p>
</div>
</div>
<div class="space-y-3">
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-surface-400">Index Size</span>
<span id="faiss-vectors" class="text-white font-mono">-- vectors</span>
</div>
</div>
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-surface-400">Memory Used</span>
<span id="faiss-memory" class="text-white font-mono">-- MB</span>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Processing Modal -->
<div id="process-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="glass-card rounded-2xl p-8 max-w-md w-full mx-4 text-center">
<div class="w-16 h-16 rounded-full bg-brand-600/20 flex items-center justify-center mx-auto mb-4 animate-pulse-glow">
<i data-lucide="brain" class="w-8 h-8 text-brand-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Running Reconciliation</h3>
<p class="text-sm text-surface-400 mb-4" id="process-step">Initializing engine...</p>
<div class="w-full bg-surface-800 rounded-full h-2 overflow-hidden">
<div id="process-bar" class="progress-bar h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
<p class="text-xs text-surface-500 mt-3 font-mono" id="process-pct">0%</p>
</div>
</div>
<!-- Toast Container -->
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
</main>
</div>
<script>
// All static dummy data arrays removed — UI is 100% backend-driven
// ============================================
// TAB NAVIGATION
// ============================================
const titles = {
dashboard: ['Dashboard', 'Financial reconciliation overview'],
reconciliation: ['Reconciliation', 'Intelligent matching with Fuzzy + AI semantic analysis'],
anomaly: ['Anomaly Detection', 'IsolationForest-powered anomaly detection'],
fraud: ['Fraud Network', 'NetworkX circular trading visualization'],
'ai-explain': ['ReconAI', 'ReconAI LLM-powered audit commentary'],
vector: ['Vector Memory', 'FAISS persistent vector index management']
};
function switchTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.getElementById('tab-' + tab).classList.remove('hidden');
document.querySelectorAll('.sidebar-link').forEach(el => {
el.classList.toggle('active', el.dataset.nav === tab);
});
if (titles[tab]) {
document.getElementById('page-title').textContent = titles[tab][0];
document.getElementById('page-subtitle').textContent = titles[tab][1];
}
// Close mobile sidebar
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
sidebar.classList.add('-translate-x-full');
overlay.classList.add('hidden');
// Initialize fraud graph if switching to fraud tab
if (tab === 'fraud') {
setTimeout(drawFraudNetwork, 100);
}
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
sidebar.classList.toggle('-translate-x-full');
overlay.classList.toggle('hidden');
}
let isDark = true;
function toggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
}
// Counter animation removed — no more data-counter attributes
async function showExplanation(invoiceId) {
const anomaly = window.anomalyDataMap ? window.anomalyDataMap[invoiceId] : null;
if (!anomaly) return;
switchTab('ai-explain');
// Add user message
addChatMessage(`Explain the anomaly for invoice ${invoiceId} and vendor ${anomaly.VendorName_books}`, false);
// Add loading state
const chatMessages = document.getElementById('chat-messages');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'chat-loading';
loadingDiv.className = 'flex gap-3 animate-slide-in';
loadingDiv.innerHTML = `
<div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
<i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
</div>
<div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
<p class="text-sm text-surface-400 flex items-center gap-2"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i> Analyzing with ReconAI...</p>
</div>
`;
chatMessages.appendChild(loadingDiv);
lucide.createIcons();
try {
const res = await fetch('/api/explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
row: anomaly,
match_status: anomaly.MatchStatus,
b_vendor: anomaly.VendorName_books,
g_vendor: anomaly.VendorName_gst,
b_amount: anomaly.Amount_books,
g_amount: anomaly.Amount_gst
})
});
const result = await res.json();
document.getElementById('chat-loading')?.remove();
if (result.explanation) {
addChatMessage(result.explanation, true);
const expList = document.getElementById('explanations-list');
if (expList) {
const div = document.createElement('div');
div.className = 'p-3 rounded-lg border border-surface-700/50 bg-surface-900/50 text-sm text-surface-200 animate-slide-in';
div.innerHTML = `
<div class="flex items-center justify-between mb-2 pb-2 border-b border-surface-800">
<span class="text-xs font-semibold text-brand-400">Invoice ${invoiceId}</span>
<span class="text-[10px] text-surface-500 font-mono">ReconAI</span>
</div>
<p class="text-xs">${result.explanation}</p>
`;
expList.prepend(div);
}
} else {
addChatMessage('Error generating explanation.', true);
}
} catch (err) {
document.getElementById('chat-loading')?.remove();
addChatMessage('API Error: ' + err.message, true);
}
}
function addChatMessage(text, isAI = false) {
const chatMessages = document.getElementById('chat-messages');
const msgDiv = document.createElement('div');
msgDiv.className = 'flex gap-3 animate-slide-in';
if (isAI) {
msgDiv.innerHTML = `
<div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
<i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
</div>
<div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
<p class="text-sm text-surface-200">${text}</p>
<p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
</div>
`;
} else {
msgDiv.innerHTML = `
<div class="w-7 h-7 rounded-full bg-purple-600/30 flex items-center justify-center shrink-0">
<i data-lucide="user" class="w-3.5 h-3.5 text-purple-400"></i>
</div>
<div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
<p class="text-sm text-surface-200">${text}</p>
</div>
`;
}
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
lucide.createIcons();
}
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text) return;
addChatMessage(text, false);
input.value = '';
// Send to live backend
try {
const res = await fetch('/api/explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ row: {}, match_status: text, b_vendor: 'N/A', g_vendor: 'N/A', b_amount: 0, g_amount: 0 })
});
const result = await res.json();
addChatMessage(result.explanation || 'No response from AI.', true);
} catch (err) {
addChatMessage('API Error: ' + err.message, true);
}
}
function askSuggested(text) {
document.getElementById('chat-input').value = text;
sendChatMessage();
}
// ============================================
// FRAUD NETWORK CANVAS
// ============================================
function drawFraudNetwork() {
const canvas = document.getElementById('fraud-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const W = rect.width;
const H = rect.height;
ctx.clearRect(0, 0, W, H);
if (!window.fraudNetworkData || !window.fraudNetworkData.nodes || window.fraudNetworkData.nodes.length === 0) {
ctx.font = '14px Inter';
ctx.fillStyle = '#64748b';
ctx.textAlign = 'center';
ctx.fillText('No fraud network data yet.', W/2, H/2 - 10);
ctx.font = '12px Inter';
ctx.fillText('Upload CSVs and Run Engine to analyze transaction networks.', W/2, H/2 + 15);
return;
}
const nodes = window.fraudNetworkData.nodes;
const edges = window.fraudNetworkData.edges;
const cycles = window.fraudNetworkData.cycles || [];
// Assign positions in a circle for better layout
const centerX = W / 2;
const centerY = H / 2;
const radius = Math.min(W, H) / 2 - 40;
nodes.forEach((node, i) => {
if (!node.x) {
const angle = (i / nodes.length) * 2 * Math.PI;
node.x = centerX + radius * Math.cos(angle);
node.y = centerY + radius * Math.sin(angle);
}
});
// Draw edges
edges.forEach(edge => {
const from = nodes[edge.from];
const to = nodes[edge.to];
if (!from || !to) return;
// check if edge is part of a cycle
let inCycle = false;
for (let c of cycles) {
const idx1 = c.indexOf(from.id);
const idx2 = c.indexOf(to.id);
if (idx1 !== -1 && idx2 !== -1) {
if ((idx1 + 1) % c.length === idx2) {
inCycle = true;
break;
}
}
}
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.strokeStyle = inCycle ? '#ef4444' : '#64748b66';
ctx.lineWidth = inCycle ? 2 : 1;
ctx.stroke();
// arrow head
const angle = Math.atan2(to.y - from.y, to.x - from.x);
const midX = from.x + (to.x - from.x) * 0.7; // Closer to target
const midY = from.y + (to.y - from.y) * 0.7;
ctx.beginPath();
ctx.moveTo(midX + 6 * Math.cos(angle), midY + 6 * Math.sin(angle));
ctx.lineTo(midX - 6 * Math.cos(angle) + 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) + 4 * Math.sin(angle + Math.PI/2));
ctx.lineTo(midX - 6 * Math.cos(angle) - 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) - 4 * Math.sin(angle + Math.PI/2));
ctx.fillStyle = inCycle ? '#ef4444' : '#64748b66';
ctx.fill();
});
// Draw nodes
nodes.forEach(node => {
let inCycle = cycles.some(c => c.includes(node.id));
const color = inCycle ? '#ef4444' : '#64748b';
ctx.beginPath();
ctx.arc(node.x, node.y, 8, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.font = '11px Inter';
ctx.fillStyle = '#e2e8f0';
ctx.textAlign = 'center';
ctx.fillText(node.label, node.x, node.y + 20);
});
}
// ============================================
// CHARTS
// ============================================
function initCharts() {
// Reconciliation Trend
const reconCtx = document.getElementById('chart-recon-trend');
if (reconCtx) {
new Chart(reconCtx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: 'Matched',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
borderColor: '#22c55e',
backgroundColor: 'rgba(34,197,94,0.1)',
fill: true,
tension: 0.4,
},
{
label: 'Discrepancies',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
borderColor: '#ef4444',
backgroundColor: 'rgba(239,68,68,0.1)',
fill: true,
tension: 0.4,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
},
scales: {
x: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
y: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
}
}
});
}
// Anomaly Distribution
const anomalyCtx = document.getElementById('chart-anomaly-dist');
if (anomalyCtx) {
new Chart(anomalyCtx, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: 'Critical',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
backgroundColor: '#ef4444',
borderRadius: 4,
},
{
label: 'High',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
backgroundColor: '#f97316',
borderRadius: 4,
},
{
label: 'Medium',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
backgroundColor: '#eab308',
borderRadius: 4,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
},
scales: {
x: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
y: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
}
}
});
}
// Match Confidence (Doughnut)
const confCtx = document.getElementById('chart-confidence');
if (confCtx) {
new Chart(confCtx, {
type: 'doughnut',
data: {
labels: ['Exact Match', 'Fuzzy Match', 'AI Semantic', 'Unmatched'],
datasets: [{
data: [0, 0, 0, 0],
backgroundColor: ['#22c55e', '#eab308', '#6366f1', '#ef4444'],
borderWidth: 0,
hoverOffset: 8,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '65%',
plugins: {
legend: {
display: false
}
}
}
});
}
}
function updateFileLabel(type) {
const fileInput = document.getElementById(`${type}-file`);
const label = document.getElementById(`lbl-${type}`);
if (fileInput.files.length > 0) {
label.textContent = fileInput.files[0].name;
label.classList.remove('hidden');
label.classList.add('inline');
}
}
async function runReconciliation() {
const booksFile = document.getElementById('books-file').files[0];
const gstFile = document.getElementById('gst-file').files[0];
if (!booksFile || !gstFile) {
showToast('Please select both Books and GST CSV files first.', 'error');
return;
}
const formData = new FormData();
formData.append('books', booksFile);
formData.append('gst', gstFile);
await executeBackendCall('/api/reconcile', formData, 'Reconciling data...');
}
async function fetchLiveGst() {
const booksFile = document.getElementById('books-file').files[0];
if (!booksFile) {
showToast('Please select Books CSV file first.', 'error');
return;
}
const formData = new FormData();
formData.append('books', booksFile);
await executeBackendCall('/api/fetch_live', formData, 'Fetching live GST and reconciling...');
}
async function executeBackendCall(endpoint, formData, startMsg) {
const modal = document.getElementById('process-modal');
const bar = document.getElementById('process-bar');
const step = document.getElementById('process-step');
const pct = document.getElementById('process-pct');
modal.classList.remove('hidden');
bar.style.width = '30%';
step.textContent = startMsg;
pct.textContent = '30%';
try {
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('API request failed');
const data = await response.json();
if (data.error) throw new Error(data.error);
bar.style.width = '100%';
step.textContent = 'Complete!';
pct.textContent = '100%';
updateDashboardWithRealData(data);
setTimeout(() => {
modal.classList.add('hidden');
showToast('Process complete! ' + data.summary.exact + ' matched.', 'success');
}, 600);
} catch (err) {
modal.classList.add('hidden');
showToast('Error: ' + err.message, 'error');
console.error(err);
}
}
function updateDashboardWithRealData(data) {
// Update Dashboard Stats
document.getElementById('stat-total-records').textContent = data.summary.total_books;
document.getElementById('stat-matched').textContent = data.summary.exact;
document.getElementById('stat-unmatched').textContent = data.summary.unmatched;
document.getElementById('stat-anomalies').textContent = data.summary.anomalies;
// Calculate percentages
const total = data.summary.total_books || 1;
const matchRate = ((data.summary.exact / total) * 100).toFixed(1);
const unmatchedRate = ((data.summary.unmatched / total) * 100).toFixed(1);
const anomalyRate = ((data.summary.anomalies / total) * 100).toFixed(1);
document.getElementById('stat-total-records-sub').textContent = 'Live data loaded';
document.getElementById('stat-matched-sub').textContent = `${matchRate}% match rate`;
document.getElementById('stat-unmatched-sub').textContent = `${unmatchedRate}% discrepancy`;
document.getElementById('stat-anomalies-sub').textContent = `${anomalyRate}% contamination`;
// For Risk and Fraud, we default to 0 since we're replacing dummy data
document.getElementById('stat-fraud-rings').textContent = '0';
document.getElementById('stat-fraud-rings-sub').textContent = 'No rings detected';
document.getElementById('stat-risk-score').innerHTML = '0.0<span class="text-sm text-surface-400">/10</span>';
document.getElementById('stat-risk-score-sub').textContent = 'Low risk level';
// Update Anomaly Tab Stats
if(document.getElementById('anomaly-trained-sub')) document.getElementById('anomaly-trained-sub').textContent = `Anomaly detection model trained on ${data.summary.total_books} records.`;
if(document.getElementById('anomaly-analyzed')) document.getElementById('anomaly-analyzed').textContent = `${data.anomalies.length}/${data.summary.anomalies}`;
let crit = 0, high = 0, med = 0;
data.anomalies.forEach(a => {
const s = a.AnomalyScore || 0;
if (s > 0.3) crit++;
else if (s > 0.1) high++;
else med++;
});
const totAnom = data.summary.anomalies || 1;
document.getElementById('anomaly-crit-count').textContent = crit;
document.getElementById('anomaly-crit-bar').style.width = `${(crit/totAnom)*100}%`;
document.getElementById('anomaly-high-count').textContent = high;
document.getElementById('anomaly-high-bar').style.width = `${(high/totAnom)*100}%`;
document.getElementById('anomaly-med-count').textContent = med;
document.getElementById('anomaly-med-bar').style.width = `${(med/totAnom)*100}%`;
// Update newly added dynamic IDs for Reconciliation and Anomaly tab stats
if(document.getElementById('recon-stat-exact')) document.getElementById('recon-stat-exact').textContent = data.summary.exact || 0;
if(document.getElementById('recon-stat-fuzzy')) document.getElementById('recon-stat-fuzzy').textContent = data.summary.fuzzy || 0;
if(document.getElementById('recon-stat-semantic')) document.getElementById('recon-stat-semantic').textContent = data.summary.semantic || 0;
if(document.getElementById('recon-stat-unmatched')) document.getElementById('recon-stat-unmatched').textContent = data.summary.unmatched || 0;
if(document.getElementById('total-count')) document.getElementById('total-count').textContent = data.summary.total_books || 0;
if(document.getElementById('showing-count')) document.getElementById('showing-count').textContent = data.reconciliation.length || 0;
if(document.getElementById('alerts-count')) document.getElementById('alerts-count').textContent = data.summary.anomalies || 0;
if(document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = crit;
if(document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totAnom;
// Update Recent Alerts
const recentAlertsDiv = document.getElementById('recent-alerts');
if (recentAlertsDiv) {
if (data.anomalies && data.anomalies.length > 0) {
// Sort by highest risk first and take top 5
const sortedAnomalies = [...data.anomalies].sort((a, b) => (b.AnomalyScore || 0) - (a.AnomalyScore || 0)).slice(0, 5);
recentAlertsDiv.innerHTML = sortedAnomalies.map(a => {
const score = a.AnomalyScore || 0;
const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
const riskClass = risk === 'Critical' ? 'bg-red-500/10 border-red-500/20 text-red-400' :
risk === 'High' ? 'bg-orange-500/10 border-orange-500/20 text-orange-400' : 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400';
const icon = risk === 'Critical' ? 'alert-octagon' : risk === 'High' ? 'alert-triangle' : 'info';
return `
<div class="p-3 rounded-lg border ${riskClass} flex gap-3 items-start">
<div class="mt-0.5"><i data-lucide="${icon}" class="w-4 h-4"></i></div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-bold uppercase tracking-wider">${risk}</span>
<span class="text-[10px] font-mono opacity-80">Score: ${score.toFixed(3)}</span>
</div>
<p class="text-xs text-surface-200">Invoice <span class="font-mono text-white">${a.InvoiceID || '-'}</span> from <span class="text-white">${a.VendorName_books || 'Unknown'}</span></p>
</div>
</div>
`;
}).join('');
// Need to re-initialize lucide icons for newly added elements
setTimeout(() => lucide.createIcons(), 50);
} else {
recentAlertsDiv.innerHTML = '<p class="text-sm text-surface-400">No active alerts.</p>';
}
}
// Fraud & FAISS Updates
if (data.faiss_stats) {
if(document.getElementById('faiss-vectors')) document.getElementById('faiss-vectors').textContent = `${data.faiss_stats.ntotal} vectors`;
if(document.getElementById('faiss-memory')) document.getElementById('faiss-memory').textContent = `${data.faiss_stats.memory_mb} MB`;
if(document.getElementById('faiss-latency')) document.getElementById('faiss-latency').textContent = `0.8ms avg`;
}
if (data.fraud_network) {
const fraudCount = data.summary.fraud_rings || 0;
if(document.getElementById('stat-fraud-rings')) document.getElementById('stat-fraud-rings').textContent = fraudCount;
if(document.getElementById('stat-fraud-rings-sub')) document.getElementById('stat-fraud-rings-sub').textContent = fraudCount > 0 ? `${fraudCount} rings detected` : 'No rings detected';
if (document.getElementById('fraud-nodes')) document.getElementById('fraud-nodes').textContent = data.fraud_network.nodes.length;
if (document.getElementById('fraud-edges')) document.getElementById('fraud-edges').textContent = data.fraud_network.edges.length;
const riskScore = (data.summary.overall_risk_score || 0).toFixed(1);
if(document.getElementById('stat-risk-score')) document.getElementById('stat-risk-score').innerHTML = `${riskScore}<span class="text-sm text-surface-400">/10</span>`;
if (fraudCount > 0) {
if(document.getElementById('stat-risk-score-sub')) document.getElementById('stat-risk-score-sub').textContent = riskScore > 5 ? 'High risk level' : 'Moderate risk level';
const ringsList = document.getElementById('fraud-rings-list');
if(ringsList) {
ringsList.innerHTML = data.fraud_network.cycles.map((cycle, i) => `
<div class="p-3 rounded-lg bg-red-500/10 border border-red-500/20">
<p class="text-xs text-red-400 font-semibold mb-1">Ring #${i+1} Detected</p>
<p class="text-xs text-surface-300 font-mono">${cycle.join(' → ')}</p>
</div>
`).join('');
}
} else {
const ringsList = document.getElementById('fraud-rings-list');
if (ringsList) ringsList.innerHTML = '<p class="text-sm text-surface-400">No fraud rings detected in current dataset.</p>';
}
// Store network data for canvas rendering
window.fraudNetworkData = data.fraud_network;
if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
drawFraudNetwork();
}
}
// Update Reconciliation Table
const reconBody = document.getElementById('recon-table-body');
reconBody.innerHTML = data.reconciliation.map(row => {
const matchType = row.MatchStatus;
const statusClass = matchType === 'Exact Match' ? 'text-green-400' :
matchType.includes('Fuzzy') ? 'text-yellow-400' :
matchType.includes('Semantic') ? 'text-brand-400' : 'text-red-400';
const confidence = matchType.includes('(') ? matchType.split('(')[1].replace(')', '') : (matchType === 'Exact Match' ? '100%' : '0%');
const matchLabel = matchType.split(' ')[0];
return `
<tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
<td class="px-4 py-3 font-mono text-xs">${row.InvoiceID || '-'}</td>
<td class="px-4 py-3 font-mono text-xs">${row.VendorName_gst || row.VendorName_books || '-'}</td>
<td class="px-4 py-3 text-xs">₹${row.Amount_books || row.Amount_gst || 0}</td>
<td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold text-white bg-surface-700">${matchLabel}</span></td>
<td class="px-4 py-3"><span class="${statusClass} text-xs font-mono">${confidence}</span></td>
<td class="px-4 py-3"><span class="w-2 h-2 rounded-full inline-block ${matchType === 'Exact Match' ? 'bg-green-400' : 'bg-red-400'}"></span></td>
</tr>
`;
}).join('');
// Update Match Confidence Chart if it exists
const confCtx = document.getElementById('chart-confidence');
if (confCtx) {
const chart = Chart.getChart(confCtx);
if (chart) {
chart.data.datasets[0].data = [
data.summary.exact || 0,
data.summary.fuzzy || 0,
data.summary.semantic || 0,
data.summary.unmatched || 0
];
chart.update();
}
}
// Update Recon Trend Chart
const trendCtx = document.getElementById('chart-recon-trend');
if (trendCtx && data.charts && data.charts.recon_trend) {
const chart = Chart.getChart(trendCtx);
if (chart) {
chart.data.datasets[0].data = data.charts.recon_trend;
if (data.charts.discrep_trend && chart.data.datasets[1]) {
chart.data.datasets[1].data = data.charts.discrep_trend;
}
chart.update();
}
}
// Update Anomaly Dist Chart
const distCtx = document.getElementById('chart-anomaly-dist');
if (distCtx && data.charts && data.charts.anomaly_dist) {
const chart = Chart.getChart(distCtx);
if (chart) {
chart.data.datasets[0].data = data.charts.anomaly_dist.critical;
chart.data.datasets[1].data = data.charts.anomaly_dist.high;
chart.data.datasets[2].data = data.charts.anomaly_dist.medium;
chart.update();
}
}
window.anomalyDataMap = {};
// Calculate anomaly counts
let critCount = 0, highCount = 0, medCount = 0;
let maxScore = 0, minScore = 1;
data.anomalies.forEach(a => {
const s = a.AnomalyScore || 0;
if (s > maxScore) maxScore = s;
if (s < minScore && s > 0) minScore = s;
if (s > 0.3) critCount++;
else if (s > 0.1) highCount++;
else medCount++;
});
if (minScore === 1 && maxScore === 0) minScore = 0; // fallback if no anomalies
const totalAnomalies = data.anomalies.length;
if (document.getElementById('anomaly-score-range')) {
document.getElementById('anomaly-score-range').textContent = totalAnomalies > 0 ? `${minScore.toFixed(2)} - ${maxScore.toFixed(2)}` : '--';
}
if (document.getElementById('anomaly-crit-count')) document.getElementById('anomaly-crit-count').textContent = critCount;
if (document.getElementById('anomaly-high-count')) document.getElementById('anomaly-high-count').textContent = highCount;
if (document.getElementById('anomaly-med-count')) document.getElementById('anomaly-med-count').textContent = medCount;
if (document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = critCount;
if (document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totalAnomalies;
if (totalAnomalies > 0) {
if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = `${(critCount/totalAnomalies)*100}%`;
if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = `${(highCount/totalAnomalies)*100}%`;
if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = `${(medCount/totalAnomalies)*100}%`;
} else {
if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = '0%';
if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = '0%';
if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = '0%';
}
// Update Anomaly Table
const anomalyBody = document.getElementById('anomaly-table-body');
anomalyBody.innerHTML = data.anomalies.map(a => {
window.anomalyDataMap[a.InvoiceID] = a;
const score = a.AnomalyScore || 0;
const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
const riskClass = risk === 'Critical' ? 'bg-red-500/20 text-red-400' :
risk === 'High' ? 'bg-orange-500/20 text-orange-400' : 'bg-yellow-500/20 text-yellow-400';
return `
<tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
<td class="px-4 py-3 font-mono text-xs">${a.InvoiceID || '-'}</td>
<td class="px-4 py-3 text-xs">${a.VendorName_books || '-'}</td>
<td class="px-4 py-3 text-xs font-mono">₹${a.Amount_books || 0}</td>
<td class="px-4 py-3"><span class="text-xs font-mono text-surface-300">${score.toFixed(3)}</span></td>
<td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold ${riskClass}">${risk}</span></td>
<td class="px-4 py-3"><button class="text-[10px] px-2 py-1 rounded bg-brand-600/20 text-brand-300" onclick="showExplanation('${a.InvoiceID}')">View</button></td>
</tr>
`;
}).join('');
}
// ============================================
// TOAST NOTIFICATION
// ============================================
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
const colors = {
success: 'border-green-500/30 bg-green-500/10',
error: 'border-red-500/30 bg-red-500/10',
info: 'border-brand-500/30 bg-brand-500/10'
};
const icons = {
success: 'check-circle',
error: 'alert-circle',
info: 'info'
};
toast.className = `flex items-start gap-3 p-4 rounded-xl border ${colors[type]} backdrop-blur-xl animate-slide-in max-w-sm`;
toast.innerHTML = `
<i data-lucide="${icons[type]}" class="w-5 h-5 ${type === 'success' ? 'text-green-400' : type === 'error' ? 'text-red-400' : 'text-brand-400'} shrink-0 mt-0.5"></i>
<p class="text-sm text-surface-200">${message}</p>
`;
container.appendChild(toast);
lucide.createIcons();
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
// ============================================
// EXPORT CSV
// ============================================
function exportCSV() {
const reconBody = document.getElementById('recon-table-body');
if (!reconBody || reconBody.rows.length === 0) {
showToast('No data to export. Run Engine first.', 'error');
return;
}
showToast('Generating CSV export...', 'info');
setTimeout(() => {
const rows = Array.from(reconBody.querySelectorAll('tr'));
const headers = ['Invoice ID', 'Vendor', 'Amount', 'Match Type', 'Confidence', 'Status'];
const csvRows = rows.map(tr => Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim()).join(','));
const csv = [headers.join(','), ...csvRows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'reconciliation_results.csv';
a.click();
URL.revokeObjectURL(url);
showToast('CSV exported successfully!', 'success');
}, 500);
}
function filterReconTable() {
// In a full implementation, this would filter the current data array.
showToast('Filtering is coming soon!', 'info');
}
// ============================================
// INIT
// ============================================
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
initCharts();
// Clear initial dummy tables
document.getElementById('recon-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">Please upload CSVs and Run Engine to view data.</td></tr>`;
document.getElementById('anomaly-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">No anomalies detected yet. Upload data and Run Engine to begin analysis.</td></tr>`;
});
// Redraw fraud network on resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
drawFraudNetwork();
}
}, 200);
});
</script>
</body>
</html>