FurqanAli12345's picture
Upload index.html with huggingface_hub
d53f7d4 verified
Raw History Blame Contribute Delete
90.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agentic Finance Analyst · Enterprise Review Platform</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #0a0f1a;
--bg-secondary: #111827;
--bg-tertiary: #1a2332;
--bg-elevated: #1e2a3d;
--border-subtle: #1f2e45;
--border-light: #2d4260;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-blue-glow: rgba(59, 130, 246, 0.25);
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-rose: #f43f5e;
--accent-violet: #8b5cf6;
--accent-cyan: #06b6d4;
--radius-card: 18px;
--radius-sm: 12px;
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
--shadow-elevated: 0 12px 36px rgba(0, 0, 0, 0.55);
--transition-fast: 0.15s ease;
--transition-smooth: 0.25s cubic-bezier(0.2, 0, 0, 1);
}
html { scroll-behavior: smooth; }
body {
background: var(--bg-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
color: var(--text-primary);
padding: 24px 32px;
display: flex;
justify-content: center;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
line-height: 1.55;
}
.dashboard { max-width: 1560px; width: 100%; }
/* ===== TOP NAV ===== */
.top-nav {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 28px;
padding-bottom: 20px; border-bottom: 1px solid var(--border-subtle);
}
.brand-area { display: flex; align-items: center; gap: 16px; }
.brand-icon {
width: 48px; height: 48px;
background: linear-gradient(135deg, var(--accent-emerald), var(--accent-cyan));
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 22px; color: white;
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3);
}
.brand-text h1 {
font-size: 19px; font-weight: 700;
letter-spacing: -0.03em;
}
.brand-text .subtitle {
font-size: 12px; font-weight: 500;
color: var(--text-secondary);
display: flex; align-items: center; gap: 8px;
margin-top: 2px;
}
.env-badge {
background: var(--bg-tertiary);
padding: 2px 10px; border-radius: 30px;
border: 1px solid var(--border-light);
font-size: 10px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--accent-emerald);
}
.nav-actions { display: flex; gap: 10px; align-items: center; }
.btn {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
color: var(--text-primary);
padding: 10px 18px; border-radius: 12px;
font-weight: 500; font-size: 13px;
font-family: 'Inter', sans-serif;
display: inline-flex; align-items: center; gap: 8px;
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.btn:hover {
background: var(--bg-tertiary);
border-color: #3b5f8a;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.btn-primary {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: white; font-weight: 600;
}
.btn-primary:hover { background: #2563eb; box-shadow: 0 4px 16px var(--accent-blue-glow); }
.btn-success {
background: var(--accent-emerald);
border-color: var(--accent-emerald);
color: white; font-weight: 600;
}
.btn-success:hover { background: #059669; box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3); }
.btn-danger {
background: rgba(244, 63, 94, 0.15);
border-color: rgba(244, 63, 94, 0.4);
color: var(--accent-rose);
}
.btn-danger:hover { background: rgba(244, 63, 94, 0.25); border-color: var(--accent-rose); }
.btn-icon-only { padding: 10px 12px; border-radius: 12px; }
/* ===== STATS BAR ===== */
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-item {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 14px 18px;
display: flex;
align-items: center;
gap: 12px;
transition: all var(--transition-fast);
}
.stat-item:hover { border-color: var(--border-light); }
.stat-icon {
width: 36px; height: 36px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.stat-icon.green { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); }
.stat-icon.blue { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
.stat-icon.violet { background: rgba(139, 92, 246, 0.15); color: var(--accent-violet); }
.stat-icon.amber { background: rgba(245, 158, 11, 0.15); color: var(--accent-amber); }
.stat-info h4 {
font-size: 10.5px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--text-muted);
}
.stat-info p {
font-size: 18px; font-weight: 700;
color: var(--text-primary);
line-height: 1.2; margin-top: 2px;
}
/* ===== MONTH NAVIGATOR ===== */
.month-navigator {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-card);
padding: 16px 24px;
margin-bottom: 20px;
display: flex; justify-content: space-between;
align-items: center; flex-wrap: wrap; gap: 14px;
box-shadow: var(--shadow-card);
}
.month-nav-left { display: flex; align-items: center; gap: 16px; }
.month-nav-left i { font-size: 18px; color: var(--accent-emerald); }
.month-nav-left h2 { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.month-nav-left p { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
.month-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
.month-tab {
padding: 8px 16px; border-radius: 10px;
font-size: 12.5px; font-weight: 500;
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
display: flex; align-items: center; gap: 6px;
white-space: nowrap;
}
.month-tab:hover {
background: var(--bg-tertiary);
border-color: var(--border-light);
color: var(--text-primary);
}
.month-tab.active {
background: rgba(16, 185, 129, 0.12);
border-color: var(--accent-emerald);
color: var(--accent-emerald);
font-weight: 600;
}
.month-tab .file-count {
background: rgba(255, 255, 255, 0.1);
font-size: 10px; font-weight: 700;
padding: 1px 7px; border-radius: 20px;
min-width: 18px; text-align: center;
}
.month-tab.active .file-count { background: rgba(16, 185, 129, 0.25); }
.month-tab.add-month {
border-style: dashed;
border-color: var(--border-light);
color: var(--text-muted);
}
.month-tab.add-month:hover {
border-color: var(--accent-emerald);
color: var(--accent-emerald);
}
/* ===== UPLOAD ZONE ===== */
.upload-hero {
background: linear-gradient(135deg, var(--bg-secondary) 0%, #16233a 100%);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-card);
padding: 28px 32px;
margin-bottom: 24px;
position: relative; overflow: hidden;
box-shadow: var(--shadow-card);
}
.upload-hero::before {
content: '';
position: absolute; top: -50%; right: -10%;
width: 500px; height: 500px;
background: radial-gradient(circle, rgba(16, 185, 129, 0.06) 0%, transparent 70%);
pointer-events: none;
}
.upload-header {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 20px;
position: relative; z-index: 1;
}
.upload-header-left { display: flex; align-items: center; gap: 14px; }
.upload-header-icon {
width: 44px; height: 44px; border-radius: 14px;
background: linear-gradient(135deg, var(--accent-emerald), var(--accent-cyan));
display: flex; align-items: center; justify-content: center;
font-size: 18px; color: white;
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3);
}
.upload-header-text h2 { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; }
.upload-header-text p { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.upload-zone {
border: 2px dashed var(--border-light);
border-radius: 16px;
padding: 36px 24px;
text-align: center;
transition: all var(--transition-smooth);
cursor: pointer;
background: rgba(11, 17, 32, 0.4);
position: relative; z-index: 1;
}
.upload-zone:hover {
border-color: var(--accent-emerald);
background: rgba(16, 185, 129, 0.04);
transform: scale(1.005);
}
.upload-zone.dragover {
border-color: var(--accent-emerald);
background: rgba(16, 185, 129, 0.08);
border-style: solid;
}
.upload-zone-icon {
font-size: 42px;
color: var(--accent-emerald);
margin-bottom: 14px; opacity: 0.8;
}
.upload-zone h3 { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
.upload-zone p { font-size: 12.5px; color: var(--text-muted); margin-bottom: 16px; }
.upload-formats {
display: flex; justify-content: center;
gap: 8px; flex-wrap: wrap; margin-top: 14px;
}
.format-pill {
background: var(--bg-tertiary);
border: 1px solid var(--border-subtle);
padding: 4px 12px; border-radius: 20px;
font-size: 11px; font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.03em;
}
.format-pill i { color: var(--accent-emerald); margin-right: 4px; }
/* ===== FILE SECTIONS ===== */
.file-sections {
display: flex; flex-direction: column;
gap: 16px; margin-bottom: 24px;
}
.month-section {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-card);
overflow: hidden;
box-shadow: var(--shadow-card);
transition: all var(--transition-smooth);
}
.month-section:hover { border-color: var(--border-light); box-shadow: var(--shadow-elevated); }
.month-section-header {
display: flex; justify-content: space-between;
align-items: center; padding: 16px 24px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-subtle);
cursor: pointer;
transition: background var(--transition-fast);
user-select: none;
}
.month-section-header:hover { background: var(--bg-elevated); }
.month-section-left { display: flex; align-items: center; gap: 14px; }
.month-section-chevron {
font-size: 12px; color: var(--text-muted);
transition: transform var(--transition-fast);
width: 16px;
}
.month-section.collapsed .month-section-chevron { transform: rotate(-90deg); }
.month-section-icon {
width: 36px; height: 36px; border-radius: 10px;
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.25);
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: var(--accent-emerald);
}
.month-section-title h3 { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; }
.month-section-title p { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
.month-section-right { display: flex; align-items: center; gap: 12px; }
.month-stat {
display: flex; align-items: center; gap: 6px;
font-size: 11px; color: var(--text-muted);
background: var(--bg-primary);
padding: 4px 12px; border-radius: 20px;
border: 1px solid var(--border-subtle);
}
.month-stat i { color: var(--accent-blue); font-size: 10px; }
.month-section-body {
padding: 20px 24px;
transition: all var(--transition-smooth);
overflow: hidden;
}
.month-section.collapsed .month-section-body {
max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
}
.uploaded-files {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.empty-state { text-align: center; padding: 32px 20px; color: var(--text-muted); }
.empty-state i { font-size: 32px; opacity: 0.3; display: block; margin-bottom: 10px; }
.empty-state p { font-size: 12.5px; }
.file-card {
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 14px 16px;
display: flex; align-items: center; gap: 12px;
transition: all var(--transition-fast);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.file-card:hover {
border-color: var(--accent-blue);
background: var(--bg-secondary);
transform: translateY(-1px);
}
.file-icon {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 16px; flex-shrink: 0;
}
.file-icon.pdf { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); border: 1px solid rgba(244, 63, 94, 0.3); }
.file-icon.excel { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
.file-icon.csv { background: rgba(139, 92, 246, 0.15); color: var(--accent-violet); border: 1px solid rgba(139, 92, 246, 0.3); }
.file-icon.img { background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border: 1px solid rgba(6, 182, 212, 0.3); }
.file-info { flex: 1; min-width: 0; }
.file-info h4 {
font-size: 12.5px; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.file-info p { font-size: 10.5px; color: var(--text-muted); margin-top: 3px; }
.file-status {
font-size: 10px; font-weight: 600;
padding: 3px 9px; border-radius: 20px;
text-transform: uppercase; letter-spacing: 0.04em;
white-space: nowrap;
}
.file-status.complete { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
.file-status.saving { background: rgba(245, 158, 11, 0.15); color: var(--accent-amber); border: 1px solid rgba(245, 158, 11, 0.3); }
.file-actions {
display: flex; gap: 4px; align-items: center;
}
.file-action-btn {
width: 32px; height: 32px; border-radius: 8px;
background: transparent;
border: 1px solid var(--border-subtle);
color: var(--text-muted);
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
font-size: 12px;
}
.file-action-btn:hover {
background: var(--bg-tertiary);
border-color: var(--border-light);
color: var(--text-primary);
}
.file-action-btn.download:hover { color: var(--accent-blue); border-color: var(--accent-blue); }
.file-action-btn.remove:hover { color: var(--accent-rose); border-color: var(--accent-rose); }
/* ===== KPI STRIP ===== */
.kpi-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.kpi-card {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 18px 22px;
transition: all var(--transition-smooth);
cursor: pointer;
position: relative; overflow: hidden;
}
.kpi-card::before {
content: '';
position: absolute; top: 0; left: 0;
width: 4px; height: 100%;
background: var(--accent-emerald);
opacity: 0; transition: opacity var(--transition-fast);
}
.kpi-card:hover {
border-color: var(--border-light);
box-shadow: var(--shadow-card);
transform: translateY(-2px);
}
.kpi-card:hover::before { opacity: 1; }
.kpi-label {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-muted); margin-bottom: 8px;
display: flex; align-items: center; gap: 6px;
}
.kpi-value {
font-size: 26px; font-weight: 700;
letter-spacing: -0.03em; line-height: 1.2;
}
.kpi-change {
font-size: 12px; font-weight: 500;
margin-top: 6px; display: flex; align-items: center; gap: 4px;
}
.kpi-change.positive { color: var(--accent-emerald); }
.kpi-change.negative { color: var(--accent-rose); }
.kpi-change.neutral { color: var(--text-muted); }
/* ===== MAIN GRID ===== */
.main-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 20px; margin-bottom: 20px;
}
.secondary-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 20px;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-card);
padding: 24px 28px;
transition: all var(--transition-smooth);
box-shadow: var(--shadow-card);
position: relative;
}
.card:hover { border-color: var(--border-light); box-shadow: var(--shadow-elevated); }
.card-header {
display: flex; justify-content: space-between;
align-items: flex-start; margin-bottom: 20px;
}
.card-title {
font-size: 15px; font-weight: 600;
display: flex; align-items: center; gap: 10px;
letter-spacing: -0.01em;
}
.card-title i { font-size: 16px; color: var(--accent-emerald); width: 20px; text-align: center; }
.card-subtitle { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.badge {
font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
padding: 4px 12px; border-radius: 20px; white-space: nowrap;
}
.badge-live { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
.badge-active { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); border: 1px solid rgba(59, 130, 246, 0.3); }
.badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--accent-amber); border: 1px solid rgba(245, 158, 11, 0.3); }
.badge-neutral { background: rgba(100, 116, 139, 0.15); color: var(--text-muted); border: 1px solid rgba(100, 116, 139, 0.3); }
.badge-success { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
.chart-container { position: relative; height: 220px; margin-top: 10px; }
/* ===== AGENT LIST ===== */
.agent-list { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.agent-item {
display: flex; align-items: center;
justify-content: space-between;
background: var(--bg-primary);
padding: 12px 16px; border-radius: 12px;
border-left: 3px solid var(--accent-blue);
transition: all var(--transition-fast);
cursor: pointer;
}
.agent-item:hover {
background: var(--bg-tertiary);
border-left-color: var(--accent-emerald);
transform: translateX(4px);
}
.agent-info { display: flex; align-items: center; gap: 12px; }
.agent-icon {
width: 34px; height: 34px; border-radius: 10px;
background: var(--bg-tertiary);
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: var(--accent-emerald);
border: 1px solid var(--border-subtle);
}
.agent-name { font-size: 13px; font-weight: 600; }
.agent-role { font-size: 10.5px; color: var(--text-muted); margin-top: 1px; }
.agent-status {
font-size: 10px; font-weight: 600;
padding: 3px 10px; border-radius: 20px;
}
.status-active { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); }
.status-processing { background: rgba(245, 158, 11, 0.15); color: var(--accent-amber); }
.status-idle { background: rgba(100, 116, 139, 0.15); color: var(--text-muted); }
/* ===== FINDINGS ===== */
.finding-item {
background: var(--bg-primary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 14px 16px; margin-bottom: 10px;
border-left: 3px solid var(--accent-amber);
transition: all var(--transition-fast);
cursor: pointer;
}
.finding-item:hover {
border-left-color: var(--accent-emerald);
background: var(--bg-tertiary);
transform: translateX(4px);
}
.finding-item.critical { border-left-color: var(--accent-rose); }
.finding-item.success { border-left-color: var(--accent-emerald); }
.finding-item.info { border-left-color: var(--accent-blue); }
.finding-header {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 6px;
}
.finding-title {
font-size: 13px; font-weight: 600;
display: flex; align-items: center; gap: 8px;
}
.finding-severity {
font-size: 9.5px; font-weight: 700;
padding: 3px 9px; border-radius: 20px;
text-transform: uppercase; letter-spacing: 0.05em;
}
.severity-critical { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); }
.severity-warning { background: rgba(245, 158, 11, 0.15); color: var(--accent-amber); }
.severity-info { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
.severity-success { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); }
.finding-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
.finding-meta {
display: flex; gap: 14px; margin-top: 8px;
font-size: 10.5px; color: var(--text-muted);
}
/* ===== DATA TABLE ===== */
.data-table-wrapper { overflow-x: auto; margin: 0 -4px; padding: 0 4px; }
.data-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.data-table thead th {
text-align: left; padding: 10px 14px;
font-size: 10.5px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-muted);
border-bottom: 1px solid var(--border-subtle);
white-space: nowrap;
}
.data-table tbody td {
padding: 12px 14px;
border-bottom: 1px solid var(--border-subtle);
color: var(--text-primary); font-weight: 400;
white-space: nowrap;
}
.data-table tbody tr { transition: background var(--transition-fast); cursor: pointer; }
.data-table tbody tr:hover { background: rgba(59, 130, 246, 0.06); }
.data-table tbody tr:last-child td { border-bottom: none; }
.text-mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11.5px; font-weight: 500; }
.text-positive { color: var(--accent-emerald); font-weight: 600; }
.text-negative { color: var(--accent-rose); font-weight: 600; }
.text-warning { color: var(--accent-amber); font-weight: 600; }
.text-muted { color: var(--text-muted); }
/* ===== LOG CONSOLE ===== */
.log-console {
background: #060b14;
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 16px 18px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11.5px; line-height: 1.7;
max-height: 240px; overflow-y: auto;
color: #b8d4ff;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.6);
margin-top: 12px;
}
.log-entry { display: flex; gap: 12px; padding: 1px 0; white-space: nowrap; }
.log-timestamp { color: #3b5a7a; flex-shrink: 0; font-size: 10.5px; }
.log-level { font-weight: 600; flex-shrink: 0; width: 72px; font-size: 10.5px; }
.log-level.info { color: #3b82f6; }
.log-level.success { color: #10b981; }
.log-level.warn { color: #f59e0b; }
.log-level.error { color: #f43f5e; }
.log-message { color: #a5c2e0; white-space: pre-wrap; word-break: break-word; }
.log-message .highlight { color: #60a5fa; font-weight: 500; }
.log-message .success { color: #34d399; }
.log-message .warn { color: #fbbf24; }
/* ===== STATUS BAR ===== */
.status-bar {
display: flex; justify-content: space-between;
align-items: center; margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border-subtle);
font-size: 12px; color: var(--text-muted);
}
.status-left { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.status-item { display: flex; align-items: center; gap: 6px; }
.status-item i { font-size: 11px; color: var(--accent-emerald); }
/* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #3b5f8a; }
/* ===== RESPONSIVE ===== */
@media (max-width: 1200px) {
.main-grid, .secondary-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
body { padding: 16px; }
.top-nav { flex-direction: column; align-items: flex-start; gap: 16px; }
.nav-actions { width: 100%; flex-wrap: wrap; }
.btn { flex: 1; justify-content: center; padding: 10px 14px; font-size: 12px; }
.kpi-strip { grid-template-columns: repeat(2, 1fr); }
.kpi-value { font-size: 20px; }
.card { padding: 18px 20px; }
.upload-hero { padding: 20px; }
.upload-header { flex-direction: column; align-items: flex-start; gap: 12px; }
.month-navigator { flex-direction: column; align-items: flex-start; }
.month-tabs { width: 100%; }
.stats-bar { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
.kpi-strip { grid-template-columns: 1fr; }
.uploaded-files { grid-template-columns: 1fr; }
.stats-bar { grid-template-columns: 1fr; }
}
/* ===== UTILITY ===== */
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.gap-2 { gap: 8px; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.mt-3 { margin-top: 24px; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.text-xs { font-size: 11px; }
.text-sm { font-size: 12px; }
#fileInput { display: none; }
.spinner {
display: inline-block;
width: 12px; height: 12px;
border: 2px solid rgba(16, 185, 129, 0.2);
border-top-color: var(--accent-emerald);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
.toast {
position: fixed;
bottom: 30px; right: 30px;
background: var(--bg-elevated);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 14px 20px;
box-shadow: var(--shadow-elevated);
display: flex; align-items: center; gap: 10px;
font-size: 13px;
z-index: 9999;
animation: slideUp 0.3s ease;
max-width: 380px;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.toast.success { border-left: 3px solid var(--accent-emerald); }
.toast.info { border-left: 3px solid var(--accent-blue); }
.toast.warn { border-left: 3px solid var(--accent-amber); }
.toast.error { border-left: 3px solid var(--accent-rose); }
.toast i { font-size: 16px; }
.toast.success i { color: var(--accent-emerald); }
.toast.info i { color: var(--accent-blue); }
.toast.warn i { color: var(--accent-amber); }
.toast.error i { color: var(--accent-rose); }
/* Loading state */
.loading-overlay {
position: fixed; inset: 0;
background: rgba(10, 15, 26, 0.85);
backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
z-index: 10000;
flex-direction: column; gap: 20px;
}
.loading-spinner {
width: 48px; height: 48px;
border: 4px solid rgba(16, 185, 129, 0.2);
border-top-color: var(--accent-emerald);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
</style>
</head>
<body>
<div class="dashboard">
<!-- ===== TOP NAV ===== -->
<nav class="top-nav">
<div class="brand-area">
<div class="brand-icon">
<i class="fas fa-chart-pie"></i>
</div>
<div class="brand-text">
<h1>Agentic Finance Analyst</h1>
<div class="subtitle">
<span>Enterprise Review Platform</span>
<span class="env-badge"><i class="fas fa-circle" style="font-size: 6px; margin-right: 4px;"></i>Persistent Storage</span>
</div>
</div>
</div>
<div class="nav-actions">
<button class="btn btn-danger" id="clearAllBtn" title="Clear all stored data">
<i class="fas fa-trash-can"></i> Clear All
</button>
<button class="btn" id="exportBtn">
<i class="fas fa-download"></i> Export
</button>
<button class="btn btn-success" id="analyzeBtn">
<i class="fas fa-wand-magic-sparkles"></i> Run Analysis
</button>
</div>
</nav>
<!-- ===== STATS BAR ===== -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-icon green"><i class="fas fa-folder"></i></div>
<div class="stat-info">
<h4>Active Periods</h4>
<p id="statMonths">0</p>
</div>
</div>
<div class="stat-item">
<div class="stat-icon blue"><i class="fas fa-file"></i></div>
<div class="stat-info">
<h4>Total Files</h4>
<p id="statFiles">0</p>
</div>
</div>
<div class="stat-item">
<div class="stat-icon violet"><i class="fas fa-database"></i></div>
<div class="stat-info">
<h4>Storage Used</h4>
<p id="statSize">0 B</p>
</div>
</div>
<div class="stat-item">
<div class="stat-icon amber"><i class="fas fa-clock"></i></div>
<div class="stat-info">
<h4>Last Upload</h4>
<p id="statLast">—</p>
</div>
</div>
</div>
<!-- ===== MONTH NAVIGATOR ===== -->
<div class="month-navigator">
<div class="month-nav-left">
<i class="fas fa-calendar-days"></i>
<div>
<h2>Financial Periods</h2>
<p>Files are auto-saved to IndexedDB and survive refresh</p>
</div>
</div>
<div class="month-tabs" id="monthTabs"></div>
</div>
<!-- ===== UPLOAD HERO ===== -->
<div class="upload-hero">
<div class="upload-header">
<div class="upload-header-left">
<div class="upload-header-icon">
<i class="fas fa-cloud-arrow-up"></i>
</div>
<div class="upload-header-text">
<h2>Upload Financial Documents</h2>
<p>Files are auto-saved to <strong id="currentMonthLabel">Current Month</strong> · Persistent IndexedDB storage</p>
</div>
</div>
<span class="badge badge-live"><i class="fas fa-shield-halved" style="margin-right: 4px;"></i>Auto-Saved</span>
</div>
<div class="upload-zone" id="uploadZone">
<div class="upload-zone-icon">
<i class="fas fa-file-invoice-dollar"></i>
</div>
<h3>Drop files here or click to browse</h3>
<p>Files persist across browser sessions · Max 50MB per file</p>
<div class="upload-formats">
<span class="format-pill"><i class="fas fa-file-pdf"></i>PDF</span>
<span class="format-pill"><i class="fas fa-file-excel"></i>XLSX</span>
<span class="format-pill"><i class="fas fa-file-csv"></i>CSV</span>
<span class="format-pill"><i class="fas fa-image"></i>Images</span>
</div>
<input type="file" id="fileInput" multiple accept=".pdf,.xlsx,.xls,.csv,.png,.jpg,.jpeg">
</div>
</div>
<!-- ===== FILE SECTIONS ===== -->
<div class="file-sections" id="fileSections"></div>
<!-- ===== KPI STRIP ===== -->
<div class="kpi-strip">
<div class="kpi-card" data-kpi="revenue">
<div class="kpi-label"><i class="fas fa-dollar-sign"></i> Total Revenue</div>
<div class="kpi-value" id="kpiRevenue">$0</div>
<div class="kpi-change positive"><i class="fas fa-arrow-trend-up"></i> +12.4% YoY</div>
</div>
<div class="kpi-card" data-kpi="expenses">
<div class="kpi-label"><i class="fas fa-receipt"></i> Total Expenses</div>
<div class="kpi-value" id="kpiExpenses">$0</div>
<div class="kpi-change negative"><i class="fas fa-arrow-trend-up"></i> +8.7% YoY</div>
</div>
<div class="kpi-card" data-kpi="profit">
<div class="kpi-label"><i class="fas fa-chart-line"></i> Net Profit</div>
<div class="kpi-value" id="kpiProfit">$0</div>
<div class="kpi-change positive"><i class="fas fa-arrow-trend-up"></i> +18.2%</div>
</div>
<div class="kpi-card" data-kpi="margin">
<div class="kpi-label"><i class="fas fa-percent"></i> Profit Margin</div>
<div class="kpi-value" id="kpiMargin">0%</div>
<div class="kpi-change positive"><i class="fas fa-arrow-trend-up"></i> +1.5pp</div>
</div>
<div class="kpi-card" data-kpi="cashflow">
<div class="kpi-label"><i class="fas fa-money-bill-wave"></i> Cash Flow</div>
<div class="kpi-value" id="kpiCashflow">$0</div>
<div class="kpi-change positive"><i class="fas fa-arrow-trend-up"></i> +6.1%</div>
</div>
</div>
<!-- ===== MAIN GRID ===== -->
<div class="main-grid">
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-chart-column"></i> Revenue vs Expenses</div>
<div class="card-subtitle">Last 12 months · Auto-generated</div>
</div>
<span class="badge badge-active" id="chartStatusBadge">Live</span>
</div>
<div class="chart-container">
<canvas id="financialChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-users-gear"></i> AI Analyst Crew</div>
<div class="card-subtitle">4 specialist agents</div>
</div>
<span class="badge badge-live"><i class="fas fa-circle" style="font-size: 6px; margin-right: 4px;"></i>Ready</span>
</div>
<div class="agent-list">
<div class="agent-item">
<div class="agent-info">
<div class="agent-icon"><i class="fas fa-file-import"></i></div>
<div>
<div class="agent-name">Document Ingestion</div>
<div class="agent-role">Parses PDFs, Excel & CSV</div>
</div>
</div>
<span class="agent-status status-active" id="agentIngestion">Ready</span>
</div>
<div class="agent-item">
<div class="agent-info">
<div class="agent-icon"><i class="fas fa-magnifying-glass-chart"></i></div>
<div>
<div class="agent-name">Financial Extractor</div>
<div class="agent-role">Normalizes line items</div>
</div>
</div>
<span class="agent-status status-idle" id="agentExtraction">Idle</span>
</div>
<div class="agent-item">
<div class="agent-info">
<div class="agent-icon"><i class="fas fa-calculator"></i></div>
<div>
<div class="agent-name">Financial Analyst</div>
<div class="agent-role">Ratios & anomalies</div>
</div>
</div>
<span class="agent-status status-idle" id="agentAnalysis">Idle</span>
</div>
<div class="agent-item">
<div class="agent-info">
<div class="agent-icon"><i class="fas fa-lightbulb"></i></div>
<div>
<div class="agent-name">Strategy Advisor</div>
<div class="agent-role">Recommendations</div>
</div>
</div>
<span class="agent-status status-idle" id="agentAdvisor">Idle</span>
</div>
</div>
<div class="mt-2 text-xs text-muted" style="font-size: 0.7rem;">
<i class="fas fa-circle" style="color: var(--accent-emerald); font-size: 5px;"></i>
<span id="agentSummary">Awaiting documents</span>
</div>
</div>
</div>
<!-- ===== SECONDARY GRID ===== -->
<div class="secondary-grid">
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-clipboard-check"></i> Key Findings</div>
<div class="card-subtitle">AI-generated insights</div>
</div>
<span class="badge badge-warning" id="findingsCount">0 findings</span>
</div>
<div id="findingsList">
<div class="finding-item info">
<div class="finding-header">
<div class="finding-title"><i class="fas fa-info-circle" style="color: var(--accent-blue);"></i> Awaiting Documents</div>
<span class="finding-severity severity-info">Info</span>
</div>
<div class="finding-desc">Upload files to generate insights. All files are saved persistently in your browser.</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-chart-pie"></i> Expense Breakdown</div>
<div class="card-subtitle">By category</div>
</div>
<span class="badge badge-neutral">FY 2026</span>
</div>
<div class="chart-container" style="height: 200px;">
<canvas id="expenseChart"></canvas>
</div>
<div class="mt-2" id="expenseLegend"></div>
</div>
</div>
<!-- ===== DATA TABLE + LOGS ===== -->
<div class="secondary-grid" style="margin-top: 20px;">
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-table-list"></i> Extracted Financial Data</div>
<div class="card-subtitle">Normalized line items</div>
</div>
<span class="badge badge-active" id="dataRowsBadge">0 rows</span>
</div>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Account</th>
<th>Category</th>
<th>Amount</th>
<th>Variance</th>
<th>Status</th>
</tr>
</thead>
<tbody id="dataTableBody">
<tr>
<td colspan="5" class="text-muted" style="text-align: center; padding: 30px;">
<i class="fas fa-inbox" style="font-size: 24px; opacity: 0.3; display: block; margin-bottom: 8px;"></i>
No data yet
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title"><i class="fas fa-terminal"></i> Processing Console</div>
<div class="card-subtitle">Real-time activity</div>
</div>
<span class="badge badge-live"><i class="fas fa-circle" style="font-size: 6px; margin-right: 4px;"></i>Streaming</span>
</div>
<div class="log-console" id="logConsole">
<div class="log-entry">
<span class="log-timestamp">--:--:--</span>
<span class="log-level info">[INFO]</span>
<span class="log-message"><span class="highlight">[System]</span> Initializing persistent storage...</span>
</div>
</div>
<div class="mt-2 flex-between text-xs text-muted">
<span><i class="fas fa-shield-halved" style="color: var(--accent-emerald);"></i> IndexedDB active</span>
<span><i class="fas fa-database"></i> <span id="totalDocsCount">0</span> files stored</span>
</div>
</div>
</div>
<!-- ===== STATUS BAR ===== -->
<div class="status-bar">
<div class="status-left">
<div class="status-item"><i class="fas fa-server"></i> Parser: Operational</div>
<div class="status-item"><i class="fas fa-database"></i> Storage: IndexedDB</div>
<div class="status-item"><i class="fas fa-shield-halved"></i> Persistent</div>
<div class="status-item"><i class="fas fa-clock"></i> Uptime: 99.99%</div>
</div>
<div class="status-left">
<div class="status-item"><i class="fas fa-code-branch"></i> Build 5.0.0</div>
<div class="status-item"><i class="fas fa-globe"></i> Region: Local</div>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ============================================================
// INDEXEDDB PERSISTENT STORAGE
// ============================================================
const DB_NAME = 'AgenticFinanceDB';
const DB_VERSION = 1;
const STORE_FILES = 'files'; // File metadata + blob
const STORE_META = 'meta'; // App metadata
let db = null;
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => {
const database = e.target.result;
if (!database.objectStoreNames.contains(STORE_FILES)) {
const store = database.createObjectStore(STORE_FILES, { keyPath: 'id' });
store.createIndex('month', 'month', { unique: false });
store.createIndex('uploadedAt', 'uploadedAt', { unique: false });
}
if (!database.objectStoreNames.contains(STORE_META)) {
database.createObjectStore(STORE_META, { keyPath: 'key' });
}
};
req.onsuccess = (e) => { db = e.target.result; resolve(db); };
req.onerror = (e) => reject(e.target.error);
});
}
// Store a file (with blob) in IndexedDB
function dbPutFile(fileRecord) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_FILES, 'readwrite');
const store = tx.objectStore(STORE_FILES);
const req = store.put(fileRecord);
req.onsuccess = () => resolve(fileRecord);
req.onerror = (e) => reject(e.target.error);
});
}
// Get all files
function dbGetAllFiles() {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_FILES, 'readonly');
const store = tx.objectStore(STORE_FILES);
const req = store.getAll();
req.onsuccess = () => resolve(req.result || []);
req.onerror = (e) => reject(e.target.error);
});
}
// Get files by month
function dbGetFilesByMonth(month) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_FILES, 'readonly');
const store = tx.objectStore(STORE_FILES);
const idx = store.index('month');
const req = idx.getAll(month);
req.onsuccess = () => resolve(req.result || []);
req.onerror = (e) => reject(e.target.error);
});
}
// Delete file by id
function dbDeleteFile(id) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_FILES, 'readwrite');
const store = tx.objectStore(STORE_FILES);
const req = store.delete(id);
req.onsuccess = () => resolve();
req.onerror = (e) => reject(e.target.error);
});
}
// Clear all files
function dbClearAll() {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_FILES, 'readwrite');
const store = tx.objectStore(STORE_FILES);
const req = store.clear();
req.onsuccess = () => resolve();
req.onerror = (e) => reject(e.target.error);
});
}
// Get / set meta
function dbGetMeta(key) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_META, 'readonly');
const store = tx.objectStore(STORE_META);
const req = store.get(key);
req.onsuccess = () => resolve(req.result ? req.result.value : null);
req.onerror = (e) => reject(e.target.error);
});
}
function dbSetMeta(key, value) {
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_META, 'readwrite');
const store = tx.objectStore(STORE_META);
const req = store.put({ key, value });
req.onsuccess = () => resolve();
req.onerror = (e) => reject(e.target.error);
});
}
// ============================================================
// STATE
// ============================================================
const state = {
activeMonth: null,
files: [], // all file records from DB
chart: null,
expenseChart: null,
processed: false,
collapsedMonths: new Set()
};
// ============================================================
// DOM REFS
// ============================================================
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const monthTabs = document.getElementById('monthTabs');
const fileSections = document.getElementById('fileSections');
const analyzeBtn = document.getElementById('analyzeBtn');
const logConsole = document.getElementById('logConsole');
const currentMonthLabel = document.getElementById('currentMonthLabel');
const totalDocsCount = document.getElementById('totalDocsCount');
// ============================================================
// HELPERS
// ============================================================
function getMonthKey(date = new Date()) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
function getMonthLabel(key) {
const [y, m] = key.split('-');
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
return `${monthNames[parseInt(m) - 1]} ${y}`;
}
function getMonthShortLabel(key) {
const [y, m] = key.split('-');
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${monthNames[parseInt(m) - 1]} ${y.slice(2)}`;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function getFileType(name) {
const ext = name.split('.').pop().toLowerCase();
if (ext === 'pdf') return { class: 'pdf', icon: 'fa-file-pdf', label: 'PDF' };
if (ext === 'xlsx' || ext === 'xls') return { class: 'excel', icon: 'fa-file-excel', label: 'Excel' };
if (ext === 'csv') return { class: 'csv', icon: 'fa-file-csv', label: 'CSV' };
if (['png', 'jpg', 'jpeg'].includes(ext)) return { class: 'img', icon: 'fa-image', label: 'Image' };
return { class: 'excel', icon: 'fa-file', label: 'File' };
}
function genId() {
return 'file_' + Date.now() + '_' + Math.random().toString(36).slice(2, 9);
}
function addLog(level, source, message) {
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0];
const levelMap = {
'info': { label: '[INFO]', cls: 'info' },
'success': { label: '[OK]', cls: 'success' },
'warn': { label: '[WARN]', cls: 'warn' },
'error': { label: '[ERROR]', cls: 'error' }
};
const lvl = levelMap[level] || levelMap.info;
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-timestamp">${timeStr}</span>
<span class="log-level ${lvl.cls}">${lvl.label}</span>
<span class="log-message"><span class="highlight">[${source}]</span> ${message}</span>
`;
logConsole.appendChild(entry);
logConsole.scrollTop = logConsole.scrollHeight;
while (logConsole.children.length > 60) {
logConsole.removeChild(logConsole.firstChild);
}
}
function showToast(type, message) {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: 'fa-circle-check',
info: 'fa-circle-info',
warn: 'fa-triangle-exclamation',
error: 'fa-circle-xmark'
};
toast.innerHTML = `<i class="fas ${icons[type]}"></i><span>${message}</span>`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, 3200);
}
function groupFilesByMonth(files) {
const grouped = {};
files.forEach(f => {
if (!grouped[f.month]) grouped[f.month] = [];
grouped[f.month].push(f);
});
// Sort files within each month by upload date (newest first)
Object.keys(grouped).forEach(m => {
grouped[m].sort((a, b) => new Date(b.uploadedDate) - new Date(a.uploadedDate));
});
return grouped;
}
// ============================================================
// INITIALIZATION
// ============================================================
async function init() {
try {
await openDB();
addLog('success', 'Storage', 'IndexedDB connected — persistent storage ready.');
// Load all files
state.files = await dbGetAllFiles();
addLog('info', 'Storage', `Loaded ${state.files.length} file(s) from persistent storage.`);
// Determine active month
const savedActiveMonth = await dbGetMeta('activeMonth');
const currentMonth = getMonthKey();
if (savedActiveMonth) {
state.activeMonth = savedActiveMonth;
} else {
state.activeMonth = currentMonth;
await dbSetMeta('activeMonth', currentMonth);
}
renderMonthTabs();
renderFileSections();
updateStats();
addLog('info', 'System', `Active period: ${getMonthLabel(state.activeMonth)}`);
} catch (e) {
console.error('Init failed:', e);
addLog('error', 'Storage', 'IndexedDB initialization failed: ' + e.message);
showToast('error', 'Storage initialization failed');
}
}
// ============================================================
// MONTH TABS
// ============================================================
function renderMonthTabs() {
const grouped = groupFilesByMonth(state.files);
const months = Object.keys(grouped);
// Always include active month
if (!months.includes(state.activeMonth)) {
months.push(state.activeMonth);
}
// Sort newest first
months.sort((a, b) => b.localeCompare(a));
monthTabs.innerHTML = months.map(m => {
const count = (grouped[m] || []).length;
const isActive = m === state.activeMonth;
return `
<div class="month-tab ${isActive ? 'active' : ''}" data-month="${m}">
<i class="fas fa-calendar-day" style="font-size: 10px; opacity: 0.7;"></i>
${getMonthShortLabel(m)}
<span class="file-count">${count}</span>
</div>
`;
}).join('') + `
<div class="month-tab add-month" id="addMonthBtn">
<i class="fas fa-plus"></i> New Month
</div>
`;
document.querySelectorAll('.month-tab[data-month]').forEach(tab => {
tab.addEventListener('click', async function() {
const month = this.dataset.month;
state.activeMonth = month;
await dbSetMeta('activeMonth', month);
renderMonthTabs();
renderFileSections();
updateMonthLabel();
addLog('info', 'Period', `Switched to ${getMonthLabel(month)}`);
});
});
document.getElementById('addMonthBtn')?.addEventListener('click', createNewMonth);
}
async function createNewMonth() {
const now = new Date();
const defaultVal = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const input = prompt('Enter month (YYYY-MM format):', defaultVal);
if (!input) return;
const match = input.match(/^(\d{4})-(\d{1,2})$/);
if (!match) {
showToast('error', 'Invalid format. Use YYYY-MM (e.g., 2026-03)');
return;
}
const year = match[1];
const month = String(parseInt(match[2])).padStart(2, '0');
const key = `${year}-${month}`;
state.activeMonth = key;
await dbSetMeta('activeMonth', key);
renderMonthTabs();
renderFileSections();
updateMonthLabel();
addLog('success', 'Period', `Created new period: ${getMonthLabel(key)}`);
showToast('success', `Switched to ${getMonthLabel(key)}`);
}
function updateMonthLabel() {
currentMonthLabel.textContent = getMonthLabel(state.activeMonth);
}
// ============================================================
// FILE SECTIONS
// ============================================================
function renderFileSections() {
const grouped = groupFilesByMonth(state.files);
// Show all months that have files, plus active month
const monthsToShow = Object.keys(grouped).filter(m => grouped[m].length > 0);
if (!monthsToShow.includes(state.activeMonth)) {
monthsToShow.push(state.activeMonth);
}
monthsToShow.sort((a, b) => b.localeCompare(a));
fileSections.innerHTML = monthsToShow.map(month => {
const files = grouped[month] || [];
const isCollapsed = state.collapsedMonths.has(month);
const isActive = month === state.activeMonth;
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
return `
<div class="month-section ${isCollapsed ? 'collapsed' : ''}" data-month="${month}">
<div class="month-section-header" data-month="${month}">
<div class="month-section-left">
<i class="fas fa-chevron-down month-section-chevron"></i>
<div class="month-section-icon">
<i class="fas fa-folder-open"></i>
</div>
<div class="month-section-title">
<h3>
${getMonthLabel(month)}
${isActive ? '<span class="badge badge-success" style="margin-left: 8px; font-size: 9px;">Active</span>' : ''}
</h3>
<p>${files.length} file${files.length !== 1 ? 's' : ''} · ${formatSize(totalSize)}</p>
</div>
</div>
<div class="month-section-right">
<div class="month-stat">
<i class="fas fa-file"></i> ${files.length}
</div>
${files.length > 0 ? `
<button class="btn btn-icon-only" data-clear-month="${month}" title="Clear month" style="padding: 6px 10px;">
<i class="fas fa-trash-can" style="font-size: 11px; color: var(--accent-rose);"></i>
</button>
` : ''}
</div>
</div>
<div class="month-section-body">
${files.length === 0 ? `
<div class="empty-state">
<i class="fas fa-folder-plus"></i>
<p>No files uploaded for this month yet</p>
</div>
` : `
<div class="uploaded-files">
${files.map(f => {
const type = getFileType(f.name);
return `
<div class="file-card" data-file-id="${f.id}">
<div class="file-icon ${type.class}">
<i class="fas ${type.icon}"></i>
</div>
<div class="file-info">
<h4 title="${f.name}">${f.name}</h4>
<p>${formatSize(f.size)} · ${type.label} · ${f.uploadedAt}</p>
</div>
<div class="file-actions">
<button class="file-action-btn download" data-download-id="${f.id}" title="Download">
<i class="fas fa-download"></i>
</button>
<button class="file-action-btn remove" data-remove-id="${f.id}" title="Delete">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
}).join('')}
</div>
`}
</div>
</div>
`;
}).join('');
// Collapse handlers
document.querySelectorAll('.month-section-header').forEach(header => {
header.addEventListener('click', function(e) {
if (e.target.closest('button') || e.target.closest('.file-action-btn')) return;
const month = this.dataset.month;
const section = this.closest('.month-section');
if (state.collapsedMonths.has(month)) {
state.collapsedMonths.delete(month);
section.classList.remove('collapsed');
} else {
state.collapsedMonths.add(month);
section.classList.add('collapsed');
}
});
});
// Download handlers
document.querySelectorAll('[data-download-id]').forEach(btn => {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
const id = this.dataset.downloadId;
await downloadFile(id);
});
});
// Remove handlers
document.querySelectorAll('[data-remove-id]').forEach(btn => {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
const id = this.dataset.removeId;
await removeFile(id);
});
});
// Clear month handlers
document.querySelectorAll('[data-clear-month]').forEach(btn => {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
const month = this.dataset.clearMonth;
await clearMonth(month);
});
});
}
// ============================================================
// FILE OPERATIONS
// ============================================================
async function handleFiles(fileList) {
const validExtensions = ['pdf', 'xlsx', 'xls', 'csv', 'png', 'jpg', 'jpeg'];
const validFiles = Array.from(fileList).filter(f => {
const ext = f.name.split('.').pop().toLowerCase();
return validExtensions.includes(ext);
});
if (validFiles.length === 0) {
addLog('error', 'Upload', 'No valid files detected.');
showToast('error', 'Invalid file type. Use PDF, XLSX, CSV, or images.');
return;
}
const month = state.activeMonth;
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0].slice(0, 5);
let savedCount = 0;
for (const file of validFiles) {
try {
// Read file as ArrayBuffer for persistent storage
const arrayBuffer = await file.arrayBuffer();
const record = {
id: genId(),
name: file.name,
size: file.size,
mimeType: file.type || 'application/octet-stream',
type: getFileType(file.name).label,
month: month,
uploadedAt: timeStr,
uploadedDate: now.toISOString(),
blob: arrayBuffer // Store actual file content
};
await dbPutFile(record);
state.files.push(record);
savedCount++;
addLog('success', 'Upload', `Saved "${file.name}" to ${getMonthLabel(month)} (${formatSize(file.size)})`);
} catch (err) {
console.error('Save failed for', file.name, err);
addLog('error', 'Upload', `Failed to save "${file.name}": ${err.message}`);
}
}
if (savedCount > 0) {
renderMonthTabs();
renderFileSections();
updateStats();
showToast('success', `${savedCount} file${savedCount > 1 ? 's' : ''} saved to ${getMonthLabel(month)}`);
addLog('info', 'Storage', `Persisted to IndexedDB · Total: ${state.files.length} files`);
}
}
async function downloadFile(id) {
const file = state.files.find(f => f.id === id);
if (!file) {
showToast('error', 'File not found');
return;
}
try {
const blob = new Blob([file.blob], { type: file.mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
addLog('success', 'Download', `Downloaded "${file.name}"`);
showToast('success', `Downloaded "${file.name}"`);
} catch (e) {
addLog('error', 'Download', `Failed: ${e.message}`);
showToast('error', 'Download failed');
}
}
async function removeFile(id) {
const file = state.files.find(f => f.id === id);
if (!file) return;
if (!confirm(`Remove "${file.name}"? This cannot be undone.`)) return;
try {
await dbDeleteFile(id);
state.files = state.files.filter(f => f.id !== id);
renderMonthTabs();
renderFileSections();
updateStats();
addLog('warn', 'Storage', `Removed "${file.name}"`);
showToast('info', `Removed "${file.name}"`);
} catch (e) {
addLog('error', 'Storage', `Delete failed: ${e.message}`);
showToast('error', 'Delete failed');
}
}
async function clearMonth(month) {
const files = state.files.filter(f => f.month === month);
if (files.length === 0) return;
if (!confirm(`Delete all ${files.length} file(s) from ${getMonthLabel(month)}?`)) return;
try {
for (const f of files) {
await dbDeleteFile(f.id);
}
state.files = state.files.filter(f => f.month !== month);
renderMonthTabs();
renderFileSections();
updateStats();
addLog('warn', 'Storage', `Cleared ${files.length} file(s) from ${getMonthLabel(month)}`);
showToast('warn', `Cleared ${getMonthLabel(month)}`);
} catch (e) {
addLog('error', 'Storage', `Clear failed: ${e.message}`);
showToast('error', 'Clear failed');
}
}
async function clearAllData() {
if (state.files.length === 0) {
showToast('info', 'No data to clear');
return;
}
if (!confirm(`Delete ALL ${state.files.length} file(s) from persistent storage? This cannot be undone.`)) return;
try {
await dbClearAll();
state.files = [];
renderMonthTabs();
renderFileSections();
updateStats();
addLog('warn', 'Storage', 'All files cleared from IndexedDB');
showToast('success', 'All data cleared');
} catch (e) {
addLog('error', 'Storage', `Clear all failed: ${e.message}`);
showToast('error', 'Clear failed');
}
}
function updateStats() {
const grouped = groupFilesByMonth(state.files);
const totalFiles = state.files.length;
const totalSize = state.files.reduce((sum, f) => sum + (f.size || 0), 0);
const monthsCount = Object.keys(grouped).length;
document.getElementById('statMonths').textContent = monthsCount;
document.getElementById('statFiles').textContent = totalFiles;
document.getElementById('statSize').textContent = formatSize(totalSize);
// Last upload
if (state.files.length > 0) {
const sorted = [...state.files].sort((a, b) => new Date(b.uploadedDate) - new Date(a.uploadedDate));
const last = sorted[0];
document.getElementById('statLast').textContent = last.uploadedAt || '—';
} else {
document.getElementById('statLast').textContent = '—';
}
totalDocsCount.textContent = totalFiles;
updateMonthLabel();
}
// ============================================================
// UPLOAD EVENT HANDLERS
// ============================================================
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
fileInput.value = '';
});
// ============================================================
// SAMPLE DATA / ANALYSIS
// ============================================================
function animateValue(id, start, end, duration, isCurrency, suffix = '') {
const el = document.getElementById(id);
if (!el) return;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = start + (end - start) * eased;
let display;
if (isCurrency) {
if (current >= 1000000) display = '$' + (current / 1000000).toFixed(2) + 'M';
else if (current >= 1000) display = '$' + (current / 1000).toFixed(0) + 'K';
else display = '$' + current.toFixed(0);
} else {
display = current.toFixed(1) + suffix;
}
el.textContent = display;
if (progress < 1) requestAnimationFrame(update);
else {
if (isCurrency) {
if (end >= 1000000) el.textContent = '$' + (end / 1000000).toFixed(2) + 'M';
else if (end >= 1000) el.textContent = '$' + (end / 1000).toFixed(0) + 'K';
} else {
el.textContent = end.toFixed(1) + suffix;
}
}
}
requestAnimationFrame(update);
}
function renderChart() {
const ctx = document.getElementById('financialChart').getContext('2d');
if (state.chart) state.chart.destroy();
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const revenueData = [320, 345, 368, 382, 395, 410, 425, 438, 452, 468, 485, 510].map(v => v * 1000);
const expensesData = [210, 225, 238, 245, 252, 260, 268, 275, 282, 290, 298, 310].map(v => v * 1000);
state.chart = new Chart(ctx, {
type: 'line',
data: {
labels: months,
datasets: [
{
label: 'Revenue',
data: revenueData,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.08)',
borderWidth: 2.5, tension: 0.4, fill: true,
pointBackgroundColor: '#10b981', pointRadius: 3, pointHoverRadius: 5
},
{
label: 'Expenses',
data: expensesData,
borderColor: '#f43f5e',
backgroundColor: 'rgba(244, 63, 94, 0.06)',
borderWidth: 2.5, tension: 0.4, fill: true,
pointBackgroundColor: '#f43f5e', pointRadius: 3, pointHoverRadius: 5
}
]
},
options: {
responsive: true, maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: {
position: 'top', align: 'end',
labels: {
color: '#94a3b8',
font: { family: 'Inter', size: 11, weight: '500' },
boxWidth: 10, boxHeight: 10,
usePointStyle: true, pointStyle: 'circle', padding: 16
}
},
tooltip: {
backgroundColor: '#1e2a3d',
titleColor: '#f1f5f9', bodyColor: '#94a3b8',
borderColor: '#2d4260', borderWidth: 1,
padding: 12, cornerRadius: 10,
callbacks: { label: ctx => ctx.dataset.label + ': $' + ctx.parsed.y.toLocaleString() }
}
},
scales: {
x: {
grid: { color: 'rgba(31, 46, 69, 0.5)', drawBorder: false },
ticks: { color: '#64748b', font: { family: 'Inter', size: 10.5 } }
},
y: {
grid: { color: 'rgba(31, 46, 69, 0.5)', drawBorder: false },
ticks: {
color: '#64748b', font: { family: 'Inter', size: 10.5 },
callback: v => '$' + (v / 1000).toFixed(0) + 'K'
}
}
}
}
});
}
function renderExpenseChart() {
const ctx = document.getElementById('expenseChart').getContext('2d');
if (state.expenseChart) state.expenseChart.destroy();
const categories = ['Payroll', 'Marketing', 'Operations', 'Software', 'Travel', 'Other'];
const values = [1450, 420, 380, 280, 195, 215];
const colors = ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#06b6d4', '#64748b'];
state.expenseChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: categories,
datasets: [{
data: values, backgroundColor: colors,
borderColor: '#111827', borderWidth: 3, hoverOffset: 8
}]
},
options: {
responsive: true, maintainAspectRatio: false,
cutout: '65%',
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#1e2a3d',
titleColor: '#f1f5f9', bodyColor: '#94a3b8',
borderColor: '#2d4260', borderWidth: 1,
padding: 10, cornerRadius: 8,
callbacks: { label: ctx => `${ctx.label}: $${ctx.parsed}K` }
}
}
}
});
document.getElementById('expenseLegend').innerHTML = categories.map((cat, i) => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 11.5px;">
<span style="display: flex; align-items: center; gap: 8px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: ${colors[i]};"></span>
<span style="color: var(--text-secondary);">${cat}</span>
</span>
<span style="color: var(--text-primary); font-weight: 600;">$${values[i]}K</span>
</div>
`).join('');
}
function renderFindings() {
const findings = [
{
type: 'critical', severity: 'critical', icon: 'fa-triangle-exclamation',
title: 'Marketing Spend Efficiency Declining',
desc: 'Marketing expenses increased 22% YoY but CAC rose 18%. Consider reallocating budget.',
meta: ['ROI: -4.2%', 'Action: Review Q3']
},
{
type: 'success', severity: 'success', icon: 'fa-circle-check',
title: 'Gross Margin Improvement',
desc: 'Gross margin expanded from 28.5% to 31.3% driven by cost optimization.',
meta: ['+2.8pp', 'Sustain']
},
{
type: 'warning', severity: 'warning', icon: 'fa-clock',
title: 'Accounts Receivable Aging',
desc: 'DSO increased from 42 to 51 days. $340K is over 60 days past due.',
meta: ['DSO: 51 days', 'Overdue: $340K']
},
{
type: 'info', severity: 'info', icon: 'fa-lightbulb',
title: 'Cash Reserve Optimization',
desc: 'Operating cash flow exceeds 3-month runway. Consider deploying excess.',
meta: ['Excess: $420K', 'Treasury']
}
];
document.getElementById('findingsList').innerHTML = findings.map(f => `
<div class="finding-item ${f.type}">
<div class="finding-header">
<div class="finding-title">
<i class="fas ${f.icon}" style="color: var(--accent-${f.type === 'critical' ? 'rose' : f.type === 'success' ? 'emerald' : f.type === 'warning' ? 'amber' : 'blue'});"></i>
${f.title}
</div>
<span class="finding-severity severity-${f.severity}">${f.severity}</span>
</div>
<div class="finding-desc">${f.desc}</div>
<div class="finding-meta">
${f.meta.map(m => `<span><i class="fas fa-circle" style="font-size: 4px; margin-right: 4px;"></i>${m}</span>`).join('')}
</div>
</div>
`).join('');
document.getElementById('findingsCount').textContent = findings.length + ' findings';
}
function renderDataTable() {
const rows = [
{ account: 'Revenue - Product Sales', category: 'Revenue', amount: 2840000, variance: '+14.2%', status: 'positive' },
{ account: 'Revenue - Services', category: 'Revenue', amount: 1440000, variance: '+9.8%', status: 'positive' },
{ account: 'Payroll & Benefits', category: 'Operating', amount: 1450000, variance: '+6.1%', status: 'warning' },
{ account: 'Marketing & Advertising', category: 'Operating', amount: 420000, variance: '+22.4%', status: 'negative' },
{ account: 'Software & Subscriptions', category: 'Operating', amount: 280000, variance: '-8.3%', status: 'positive' },
{ account: 'Travel & Entertainment', category: 'Discretionary', amount: 195000, variance: '+31.2%', status: 'negative' },
{ account: 'Professional Services', category: 'Operating', amount: 145000, variance: '+4.5%', status: 'warning' },
{ account: 'Office & Facilities', category: 'Operating', amount: 230000, variance: '+2.1%', status: 'neutral' }
];
document.getElementById('dataTableBody').innerHTML = rows.map(r => `
<tr>
<td style="font-weight: 500;">${r.account}</td>
<td><span class="text-muted">${r.category}</span></td>
<td class="text-mono">$${r.amount.toLocaleString()}</td>
<td class="text-${r.status === 'positive' ? 'positive' : r.status === 'negative' ? 'negative' : r.status === 'warning' ? 'warning' : 'muted'}">${r.variance}</td>
<td>
<span style="width: 6px; height: 6px; display: inline-block; border-radius: 50%; background: var(--accent-${r.status === 'positive' ? 'emerald' : r.status === 'negative' ? 'rose' : r.status === 'warning' ? 'amber' : 'text-muted'});"></span>
<span class="text-muted" style="font-size: 11px; margin-left: 6px;">${r.status === 'positive' ? 'Good' : r.status === 'negative' ? 'Review' : 'Monitor'}</span>
</td>
</tr>
`).join('');
document.getElementById('dataRowsBadge').textContent = rows.length + ' rows';
}
// ============================================================
// ANALYSIS FLOW
// ============================================================
function runAnalysis() {
if (state.files.length === 0) {
addLog('warn', 'Analysis', 'No files uploaded. Please add documents first.');
uploadZone.style.borderColor = 'var(--accent-amber)';
uploadZone.style.animation = 'shake 0.4s ease';
setTimeout(() => {
uploadZone.style.borderColor = '';
uploadZone.style.animation = '';
}, 800);
showToast('warn', 'Please upload at least one document first');
return;
}
const totalFiles = state.files.length;
addLog('info', 'Pipeline', `Starting analysis for ${totalFiles} document${totalFiles > 1 ? 's' : ''}...`);
const agents = [
{ id: 'agentIngestion', name: 'Document Ingestion', delay: 0 },
{ id: 'agentExtraction', name: 'Financial Extractor', delay: 800 },
{ id: 'agentAnalysis', name: 'Financial Analyst', delay: 1600 },
{ id: 'agentAdvisor', name: 'Strategy Advisor', delay: 2400 }
];
agents.forEach((agent, idx) => {
setTimeout(() => {
const el = document.getElementById(agent.id);
if (el) {
el.className = 'agent-status status-processing';
el.textContent = 'Processing';
}
addLog('info', agent.name, `Processing... stage ${idx + 1}/${agents.length}`);
setTimeout(() => {
if (el) {
el.className = 'agent-status status-active';
el.textContent = 'Complete';
}
}, 700);
}, agent.delay);
});
setTimeout(() => {
addLog('success', 'Pipeline', 'Analysis complete. Extracting metrics...');
animateValue('kpiRevenue', 0, 4280000, 800, true);
animateValue('kpiExpenses', 0, 2940000, 800, true);
animateValue('kpiProfit', 0, 1340000, 800, true);
animateValue('kpiMargin', 0, 31.3, 800, false, '%');
animateValue('kpiCashflow', 0, 1820000, 800, true);
renderChart();
renderExpenseChart();
renderFindings();
renderDataTable();
addLog('success', 'Analysis', `Generated insights · 4 findings · dashboard updated`);
document.getElementById('agentSummary').textContent = `${totalFiles} document${totalFiles > 1 ? 's' : ''} analyzed`;
document.getElementById('chartStatusBadge').textContent = 'Updated';
state.processed = true;
showToast('success', 'Analysis complete');
document.getElementById('findingsList').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 3400);
}
// ============================================================
// BUTTON HANDLERS
// ============================================================
analyzeBtn.addEventListener('click', runAnalysis);
document.getElementById('clearAllBtn').addEventListener('click', clearAllData);
document.getElementById('exportBtn').addEventListener('click', function() {
if (!state.processed) {
addLog('warn', 'Export', 'Run analysis first.');
showToast('warn', 'Run analysis before exporting');
return;
}
addLog('success', 'Export', 'Generating report...');
this.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> Generating...';
setTimeout(() => {
this.innerHTML = '<i class="fas fa-download"></i> Export';
addLog('success', 'Export', 'Report downloaded');
showToast('success', 'Report exported');
}, 1500);
});
document.querySelectorAll('.kpi-card').forEach(card => {
card.addEventListener('click', function() {
addLog('info', 'Dashboard', `Drilling into "${this.dataset.kpi}" metrics...`);
this.style.borderColor = 'var(--accent-emerald)';
setTimeout(() => { this.style.borderColor = ''; }, 300);
});
});
// ============================================================
// INIT ON LOAD
// ============================================================
window.addEventListener('load', () => {
init();
});
})();
</script>
</body>
</html>