Spaces:
Sleeping
Sleeping
Download app/static/index.html from Harshavard21/FinRAG: direct link, hf CLI and curl.
- Browser
- Download file 50.5 kB
-
https://huggingface.co/spaces/Harshavard21/FinRAG/resolve/main/app/static/index.html
- Command line
-
hf download hf://spaces/Harshavard21/FinRAG/app/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Harshavard21/FinRAG/resolve/main/app/static/index.html
50.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FinRAG β Indian Markets Intelligence</title> | |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>π</text></svg>"> | |
| <meta name="description" content="AI-powered financial Q&A over BSE annual reports of major Indian companies"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> | |
| <style> | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| RESET & TOKENS | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0d0f17; | |
| --bg-sidebar: #0a0c13; | |
| --bg-input: #131620; | |
| --bg-msg: #111420; | |
| --bg-hover: rgba(255,255,255,0.04); | |
| --bg-active: rgba(59,130,246,0.1); | |
| --border: rgba(255,255,255,0.07); | |
| --border-blue: rgba(59,130,246,0.3); | |
| --text: #e2e8f0; | |
| --text-muted: #64748b; | |
| --text-dim: #334155; | |
| --accent: #3b82f6; | |
| --accent2: #06b6d4; | |
| --accent3: #8b5cf6; | |
| --green: #10b981; | |
| --red: #ef4444; | |
| --amber: #f59e0b; | |
| --radius: 10px; | |
| --sidebar-w: 260px; | |
| --font: 'Inter', -apple-system, sans-serif; | |
| --mono: 'JetBrains Mono', monospace; | |
| } | |
| html, body { | |
| height: 100%; width: 100%; | |
| font-family: var(--font); | |
| background: var(--bg); | |
| color: var(--text); | |
| overflow: hidden; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| LAYOUT | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #app { display: flex; height: 100vh; width: 100vw; overflow: hidden; } | |
| /* ββ Sidebar ββ */ | |
| #sidebar { | |
| width: var(--sidebar-w); | |
| min-width: var(--sidebar-w); | |
| background: var(--bg-sidebar); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| transition: width 0.22s cubic-bezier(0.4,0,0.2,1), | |
| min-width 0.22s cubic-bezier(0.4,0,0.2,1); | |
| position: relative; | |
| z-index: 10; | |
| } | |
| #sidebar.collapsed { width: 0; min-width: 0; } | |
| /* ββ Main ββ */ | |
| #main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| SIDEBAR INTERNALS | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .sb-top { | |
| padding: 1rem 0.85rem 0.6rem; | |
| flex-shrink: 0; | |
| } | |
| .brand { | |
| display: flex; align-items: center; gap: 0.6rem; | |
| padding: 0.2rem 0 0.8rem; | |
| } | |
| .brand-icon { font-size: 1.3rem; } | |
| .brand-name { | |
| font-size: 1rem; font-weight: 700; letter-spacing: -0.02em; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .brand-sub { font-size: 0.6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; } | |
| .new-chat-btn { | |
| width: 100%; padding: 0.55rem 0.8rem; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| color: var(--text); font-family: var(--font); font-size: 0.83rem; font-weight: 500; | |
| cursor: pointer; display: flex; align-items: center; gap: 0.5rem; | |
| transition: background 0.15s, border-color 0.15s; | |
| } | |
| .new-chat-btn:hover { | |
| background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.15); | |
| } | |
| .new-chat-btn .plus { font-size: 1.1rem; font-weight: 300; color: var(--accent2); } | |
| .sb-divider { height: 1px; background: var(--border); margin: 0.5rem 0; } | |
| .sb-scroll { | |
| flex: 1; overflow-y: auto; overflow-x: hidden; | |
| padding: 0 0.5rem; | |
| } | |
| .sb-scroll::-webkit-scrollbar { width: 3px; } | |
| .sb-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; } | |
| .group-label { | |
| font-size: 0.65rem; font-weight: 600; color: var(--text-dim); | |
| text-transform: uppercase; letter-spacing: 0.08em; | |
| padding: 0.7rem 0.4rem 0.25rem; | |
| } | |
| .hist-item { | |
| display: flex; align-items: center; gap: 0.4rem; | |
| padding: 0.42rem 0.6rem; border-radius: 7px; | |
| font-size: 0.8rem; color: var(--text-muted); | |
| cursor: pointer; overflow: hidden; white-space: nowrap; | |
| text-overflow: ellipsis; | |
| transition: background 0.12s, color 0.12s; | |
| user-select: none; | |
| } | |
| .hist-item:hover { background: var(--bg-hover); color: var(--text); } | |
| .hist-item.active { background: var(--bg-active); color: #93c5fd; } | |
| .hist-item .icon { font-size: 0.75rem; flex-shrink: 0; } | |
| .hist-item .title { overflow: hidden; text-overflow: ellipsis; flex: 1; } | |
| .hist-item .del { | |
| flex-shrink: 0; font-size: 0.85rem; | |
| color: var(--text-dim); opacity: 0.35; | |
| padding: 0.1rem 0.3rem; border-radius: 4px; | |
| transition: opacity 0.15s, color 0.15s, background 0.15s; | |
| cursor: pointer; | |
| } | |
| .hist-item:hover .del { opacity: 1; color: var(--red); } | |
| .hist-item .del:hover { background: rgba(239,68,68,0.12); } | |
| .sb-footer { | |
| padding: 0.6rem 0.85rem; | |
| border-top: 1px solid var(--border); | |
| flex-shrink: 0; | |
| font-size: 0.65rem; color: var(--text-dim); line-height: 1.7; | |
| letter-spacing: 0.02em; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| TOPBAR | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #topbar { | |
| display: flex; align-items: center; gap: 0.75rem; | |
| padding: 0.7rem 1.2rem; | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| background: rgba(13,15,23,0.85); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| } | |
| .toggle-btn { | |
| background: none; border: none; cursor: pointer; | |
| color: var(--text-muted); padding: 0.3rem; border-radius: 6px; | |
| font-size: 1rem; transition: background 0.12s, color 0.12s; | |
| flex-shrink: 0; | |
| } | |
| .toggle-btn:hover { background: var(--bg-hover); color: var(--text); } | |
| /* Mode tabs */ | |
| .mode-tabs { display: flex; gap: 0.2rem; background: rgba(255,255,255,0.04); border-radius: 8px; padding: 0.2rem; } | |
| .mode-tab { | |
| padding: 0.3rem 0.9rem; border-radius: 6px; | |
| font-size: 0.78rem; font-weight: 500; color: var(--text-muted); | |
| cursor: pointer; border: none; background: none; font-family: var(--font); | |
| transition: all 0.15s; white-space: nowrap; letter-spacing: 0.01em; | |
| } | |
| .mode-tab.active { background: rgba(59,130,246,0.18); color: #93c5fd; } | |
| .mode-tab:hover:not(.active) { color: var(--text); } | |
| /* Context selects */ | |
| .ctx-row { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; } | |
| .ctx-select { | |
| background: rgba(255,255,255,0.05); border: 1px solid var(--border); | |
| border-radius: 7px; color: var(--text); font-family: var(--font); | |
| font-size: 0.78rem; padding: 0.3rem 0.6rem; cursor: pointer; | |
| outline: none; transition: border-color 0.15s; | |
| } | |
| .ctx-select:focus { border-color: var(--border-blue); } | |
| .ctx-select option { background: #1e2538; } | |
| /* Multi-company select for compare */ | |
| .multi-company-bar { | |
| display: flex; gap: 0.3rem; align-items: center; flex-wrap: wrap; | |
| } | |
| .co-pill { | |
| display: flex; align-items: center; gap: 0.25rem; | |
| background: rgba(59,130,246,0.12); border: 1px solid var(--border-blue); | |
| border-radius: 20px; padding: 0.18rem 0.55rem; | |
| font-size: 0.72rem; color: #93c5fd; font-family: var(--mono); | |
| } | |
| .co-pill .rm { cursor: pointer; opacity: 0.6; transition: opacity 0.1s; } | |
| .co-pill .rm:hover { opacity: 1; } | |
| .add-company-btn { | |
| background: rgba(255,255,255,0.04); border: 1px dashed var(--border); | |
| border-radius: 20px; padding: 0.18rem 0.7rem; | |
| font-size: 0.72rem; color: var(--text-muted); cursor: pointer; | |
| font-family: var(--font); transition: all 0.15s; | |
| } | |
| .add-company-btn:hover { border-color: var(--border-blue); color: var(--accent2); } | |
| .extract-btn { | |
| padding: 0.35rem 1rem; border-radius: 7px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| border: none; color: #fff; font-family: var(--font); | |
| font-size: 0.78rem; font-weight: 600; cursor: pointer; | |
| transition: opacity 0.15s; white-space: nowrap; | |
| } | |
| .extract-btn:hover { opacity: 0.85; } | |
| .extract-btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| CHAT THREAD | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #thread { | |
| flex: 1; overflow-y: auto; overflow-x: hidden; | |
| padding: 1.5rem 0 2rem; | |
| scroll-behavior: smooth; | |
| } | |
| #thread::-webkit-scrollbar { width: 4px; } | |
| #thread::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.07); border-radius: 2px; } | |
| /* welcome */ | |
| #welcome { | |
| display: flex; flex-direction: column; align-items: center; | |
| justify-content: center; min-height: 70vh; | |
| padding: 2rem; text-align: center; gap: 1rem; | |
| animation: fadeUp 0.4s ease; | |
| } | |
| .wlc-logo { font-size: 3.2rem; margin-bottom: 0.3rem; } | |
| .wlc-title { | |
| font-size: 2.2rem; font-weight: 800; letter-spacing: -0.04em; | |
| background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 50%, var(--accent3) 100%); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| line-height: 1.1; | |
| } | |
| .wlc-sub { font-size: 0.95rem; color: var(--text-muted); max-width: 420px; line-height: 1.65; } | |
| .sug-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.55rem; max-width: 580px; width: 100%; margin-top: 0.5rem; } | |
| .sug-card { | |
| background: rgba(255,255,255,0.03); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: 0.85rem 1rem; | |
| text-align: left; cursor: pointer; | |
| transition: background 0.15s, border-color 0.15s, transform 0.12s; | |
| user-select: none; | |
| } | |
| .sug-card:hover { | |
| background: rgba(59,130,246,0.07); border-color: var(--border-blue); | |
| transform: translateY(-1px); | |
| } | |
| .sug-icon { font-size: 1rem; margin-bottom: 0.3rem; display: block; } | |
| .sug-text { font-size: 0.8rem; color: var(--text-muted); line-height: 1.4; } | |
| /* Message wrapper */ | |
| .msg-wrap { | |
| max-width: 780px; margin: 0 auto; padding: 0 1.5rem; | |
| animation: fadeUp 0.25s ease; | |
| } | |
| .msg-wrap + .msg-wrap { margin-top: 0.15rem; } | |
| /* User message */ | |
| .msg-user { | |
| display: flex; justify-content: flex-end; margin: 0.8rem 0 0.3rem; | |
| } | |
| .msg-user .bubble { | |
| background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(6,182,212,0.12)); | |
| border: 1px solid rgba(59,130,246,0.2); | |
| border-radius: 14px 14px 3px 14px; | |
| padding: 0.7rem 1rem; max-width: 75%; | |
| font-size: 0.9rem; line-height: 1.6; color: #dbeafe; | |
| } | |
| /* Assistant message */ | |
| .msg-asst { display: flex; gap: 0.75rem; margin: 0.5rem 0; } | |
| .asst-avatar { | |
| width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 0.85rem; margin-top: 0.15rem; | |
| } | |
| .asst-body { flex: 1; min-width: 0; } | |
| .asst-name { font-size: 0.72rem; color: var(--text-dim); margin-bottom: 0.3rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; } | |
| .asst-content { font-size: 0.9rem; line-height: 1.75; color: #cbd5e1; } | |
| .asst-content p { margin-bottom: 0.6rem; } | |
| .asst-content p:last-child { margin-bottom: 0; } | |
| .asst-content strong { color: #e2e8f0; font-weight: 600; } | |
| .asst-content em { color: var(--accent2); } | |
| .asst-content ul, .asst-content ol { padding-left: 1.4rem; margin: 0.4rem 0; } | |
| .asst-content li { margin: 0.2rem 0; } | |
| .asst-content table { width: 100%; border-collapse: collapse; margin: 0.6rem 0; font-size: 0.83rem; } | |
| .asst-content th { background: rgba(59,130,246,0.1); border: 1px solid var(--border-blue); padding: 0.4rem 0.7rem; text-align: left; color: #93c5fd; } | |
| .asst-content td { border: 1px solid var(--border); padding: 0.35rem 0.7rem; color: var(--text-muted); } | |
| .asst-content tr:nth-child(even) td { background: rgba(255,255,255,0.02); } | |
| .asst-content code { background: rgba(255,255,255,0.08); border-radius: 4px; padding: 0.1em 0.35em; font-family: var(--mono); font-size: 0.85em; color: var(--accent2); } | |
| .asst-content pre { background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: 8px; padding: 0.8rem 1rem; overflow-x: auto; margin: 0.5rem 0; } | |
| .asst-content h3, .asst-content h4 { color: #e2e8f0; margin: 0.8rem 0 0.3rem; font-weight: 600; } | |
| /* Typing cursor */ | |
| .cursor { | |
| display: inline-block; width: 2px; height: 1em; | |
| background: var(--accent2); margin-left: 2px; | |
| vertical-align: middle; border-radius: 1px; | |
| animation: blink 0.9s infinite; | |
| } | |
| @keyframes blink { 0%,50% { opacity: 1; } 51%,100% { opacity: 0; } } | |
| /* Sources */ | |
| .sources-bar { | |
| display: flex; flex-wrap: wrap; gap: 0.3rem; | |
| margin-top: 0.7rem; padding-top: 0.6rem; | |
| border-top: 1px solid var(--border); | |
| } | |
| .src-chip { | |
| background: rgba(59,130,246,0.08); border: 1px solid rgba(59,130,246,0.3); | |
| border-radius: 5px; padding: 0.15rem 0.5rem; | |
| font-size: 0.67rem; color: #93c5fd; font-family: var(--mono); | |
| transition: all 0.12s; text-decoration: none; cursor: pointer; | |
| } | |
| .src-chip:hover { background: rgba(59,130,246,0.15); border-color: rgba(59,130,246,0.5); } | |
| /* KPI Grid */ | |
| .kpi-section { margin: 0.8rem 0; } | |
| .kpi-section-title { font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.5rem; } | |
| .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.55rem; margin-bottom: 0.55rem; } | |
| .kpi-card { | |
| background: rgba(255,255,255,0.03); border: 1px solid var(--border); | |
| border-radius: 9px; padding: 0.8rem 0.9rem; text-align: center; | |
| transition: border-color 0.15s; | |
| } | |
| .kpi-card.has-val { border-color: rgba(59,130,246,0.2); } | |
| .kpi-card .val { | |
| font-size: 1.15rem; font-weight: 700; font-family: var(--mono); | |
| color: var(--accent2); line-height: 1.2; margin-bottom: 0.25rem; | |
| } | |
| .kpi-card .val.na { color: var(--text-dim); font-size: 0.85rem; font-family: var(--font); } | |
| .kpi-card .val.text-green { color: #10b981; } | |
| .kpi-card .val.text-red { color: #ef4444; } | |
| .kpi-card .lbl { font-size: 0.63rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; } | |
| /* chart wrapper */ | |
| .chart-wrap { margin: 0.5rem 0; border-radius: var(--radius); overflow: hidden; } | |
| /* Spinner */ | |
| .spin-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0; font-size: 0.8rem; color: var(--text-dim); } | |
| .spinner { | |
| width: 14px; height: 14px; border: 2px solid rgba(59,130,246,0.15); | |
| border-top-color: var(--accent); border-radius: 50%; | |
| animation: spin 0.7s linear infinite; flex-shrink: 0; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| INPUT AREA | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #input-area { | |
| flex-shrink: 0; | |
| padding: 0.8rem 1.5rem 1.2rem; | |
| background: linear-gradient(to top, var(--bg) 80%, transparent); | |
| } | |
| .input-wrap { | |
| max-width: 780px; margin: 0 auto; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| display: flex; flex-direction: column; | |
| } | |
| .input-wrap:focus-within { | |
| border-color: var(--border-blue); | |
| box-shadow: 0 0 0 3px rgba(59,130,246,0.08); | |
| } | |
| .input-row { | |
| display: flex; align-items: flex-end; gap: 0.5rem; | |
| padding: 0.55rem 0.65rem 0.55rem 0.65rem; | |
| } | |
| #query-input { | |
| flex: 1; resize: none; border: none; outline: none; | |
| background: transparent; color: var(--text); | |
| font-family: var(--font); font-size: 0.92rem; line-height: 1.55; | |
| min-height: 24px; max-height: 180px; | |
| overflow-y: auto; padding: 0; | |
| } | |
| #query-input::placeholder { color: var(--text-dim); } | |
| #query-input::-webkit-scrollbar { width: 3px; } | |
| .input-left { display: flex; align-items: flex-end; gap: 0.3rem; } | |
| .input-right { display: flex; align-items: flex-end; } | |
| .at-btn { | |
| width: 30px; height: 30px; border-radius: 7px; border: none; | |
| background: rgba(255,255,255,0.05); color: var(--text-muted); | |
| font-size: 0.85rem; cursor: pointer; display: flex; align-items: center; justify-content: center; | |
| transition: background 0.12s, color 0.12s; position: relative; flex-shrink: 0; | |
| } | |
| .at-btn:hover { background: rgba(255,255,255,0.1); color: var(--text); } | |
| .send-btn { | |
| width: 32px; height: 32px; border-radius: 8px; border: none; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| color: #fff; font-size: 1rem; cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: opacity 0.15s, transform 0.12s; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover { opacity: 0.85; transform: scale(1.04); } | |
| .send-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; } | |
| /* Company picker popup */ | |
| #company-picker { | |
| position: absolute; bottom: calc(100% + 8px); left: 0; | |
| background: #1a1f33; border: 1px solid var(--border-blue); | |
| border-radius: 10px; width: 280px; max-height: 260px; | |
| overflow-y: auto; z-index: 100; display: none; | |
| box-shadow: 0 16px 40px rgba(0,0,0,0.5); | |
| } | |
| #company-picker.open { display: block; animation: popUp 0.15s ease; } | |
| .picker-search { | |
| padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); | |
| position: sticky; top: 0; background: #1a1f33; | |
| } | |
| .picker-search input { | |
| width: 100%; background: none; border: none; outline: none; | |
| color: var(--text); font-family: var(--font); font-size: 0.82rem; | |
| } | |
| .picker-search input::placeholder { color: var(--text-dim); } | |
| .picker-item { | |
| padding: 0.45rem 0.8rem; font-size: 0.8rem; color: var(--text-muted); | |
| cursor: pointer; display: flex; justify-content: space-between; align-items: center; | |
| transition: background 0.1s; | |
| } | |
| .picker-item:hover { background: var(--bg-hover); color: var(--text); } | |
| .picker-item .ticker { font-size: 0.67rem; font-family: var(--mono); color: var(--text-dim); } | |
| /* no hint row β removed */ | |
| /* βββββββββββββββββββββββββββββββββββββββββββ | |
| ANIMATIONS | |
| βββββββββββββββββββββββββββββββββββββββββββ */ | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes popUp { | |
| from { opacity: 0; transform: translateY(6px) scale(0.97); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- βββ SIDEBAR βββ --> | |
| <aside id="sidebar"> | |
| <div class="sb-top"> | |
| <div class="brand"> | |
| <span class="brand-icon">π</span> | |
| <div> | |
| <div class="brand-name">FinRAG</div> | |
| <div class="brand-sub">Indian Markets Intelligence</div> | |
| </div> | |
| </div> | |
| <button class="new-chat-btn" onclick="newChat()"> | |
| <span class="plus">+</span> New Chat | |
| </button> | |
| </div> | |
| <div class="sb-divider"></div> | |
| <div class="sb-scroll" id="history-list"> | |
| <!-- populated by JS --> | |
| </div> | |
| <div class="sb-footer"> | |
| Hybrid RAG Β· Financial Intelligence | |
| </div> | |
| </aside> | |
| <!-- βββ MAIN βββ --> | |
| <div id="main"> | |
| <!-- Topbar --> | |
| <div id="topbar"> | |
| <button class="toggle-btn" onclick="toggleSidebar()" title="Toggle sidebar">β°</button> | |
| <div class="mode-tabs"> | |
| <button class="mode-tab active" data-mode="chat" onclick="setMode('chat')">Chat</button> | |
| <button class="mode-tab" data-mode="compare" onclick="setMode('compare')">Compare</button> | |
| <button class="mode-tab" data-mode="dashboard" onclick="setMode('dashboard')">Dashboard</button> | |
| </div> | |
| <!-- Context (changes per mode) --> | |
| <div class="ctx-row" id="ctx-row"> | |
| <!-- rendered by renderCtx() --> | |
| </div> | |
| </div> | |
| <!-- Chat Thread --> | |
| <div id="thread"> | |
| <div id="welcome"> | |
| <div class="wlc-logo"></div> | |
| <div class="wlc-title">FinRAG</div> | |
| <div class="wlc-sub">Ask anything about major Indian companies.<br>Answers grounded in official BSE annual reports.</div> | |
| <div class="sug-grid" id="suggestions"></div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div id="input-area"> | |
| <div style="max-width:780px;margin:0 auto;position:relative;"> | |
| <div id="company-picker" class="closed"> | |
| <div class="picker-search"> | |
| <input type="text" id="picker-search" placeholder="Search companyβ¦" oninput="filterPicker(this.value)"> | |
| </div> | |
| <div id="picker-list"></div> | |
| </div> | |
| <div class="input-wrap"> | |
| <div class="input-row"> | |
| <div class="input-left"> | |
| <button class="at-btn" onclick="togglePicker()" title="Select company">@</button> | |
| </div> | |
| <textarea id="query-input" placeholder="Ask anything⦠or type @Company to filter" rows="1" | |
| onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea> | |
| <div class="input-right"> | |
| <button class="send-btn" id="send-btn" onclick="submitQuery()">β</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div><!-- end main --> | |
| </div><!-- end app --> | |
| <script> | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STATE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const state = { | |
| mode: 'chat', | |
| company: 'TCS', | |
| fy: 'FY2025', | |
| compareCompanies: ['TCS', 'INFOSYS'], | |
| conversationId: null, | |
| isStreaming: false, | |
| companies: [], | |
| tickers: {}, | |
| }; | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| INIT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function init() { | |
| // Load companies | |
| const res = await fetch('/api/companies'); | |
| const data = await res.json(); | |
| state.companies = data.companies; | |
| state.tickers = data.tickers; | |
| state.company = state.companies[0]; | |
| renderCtx(); | |
| renderSuggestions(); | |
| buildPicker(); | |
| loadHistory(); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SIDEBAR TOGGLE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function toggleSidebar() { | |
| document.getElementById('sidebar').classList.toggle('collapsed'); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MODE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function setMode(mode) { | |
| state.mode = mode; | |
| document.querySelectorAll('.mode-tab').forEach(t => t.classList.toggle('active', t.dataset.mode === mode)); | |
| renderCtx(); | |
| } | |
| function renderCtx() { | |
| const row = document.getElementById('ctx-row'); | |
| const cos = state.companies; | |
| const fyOptions = ['FY2025', 'FY2024', 'All']; | |
| if (state.mode === 'compare') { | |
| let html = `<div class="multi-company-bar" id="compare-bar">`; | |
| state.compareCompanies.forEach(c => { | |
| html += `<span class="co-pill">${c} <span class="rm" onclick="removeCompare('${c}')">Γ</span></span>`; | |
| }); | |
| html += `<button class="add-company-btn" onclick="openComparePicker()">+ Add</button></div>`; | |
| html += fySelect(); | |
| row.innerHTML = html; | |
| } else if (state.mode === 'dashboard') { | |
| row.innerHTML = companySelect() + fySelect() + | |
| `<button class="extract-btn" id="extract-btn" onclick="extractDashboard()">Extract KPIs β</button>`; | |
| } else { | |
| row.innerHTML = companySelect() + fySelect(); | |
| } | |
| } | |
| function companySelect() { | |
| let s = `<select class="ctx-select" id="company-sel" onchange="state.company=this.value">`; | |
| state.companies.forEach(c => { | |
| s += `<option value="${c}" ${c === state.company ? 'selected' : ''}>${c}</option>`; | |
| }); | |
| return s + '</select>'; | |
| } | |
| function fySelect() { | |
| return `<select class="ctx-select" id="fy-sel" onchange="state.fy=this.value"> | |
| <option value="FY2025">FY2025</option> | |
| <option value="FY2024">FY2024</option> | |
| <option value="All">All Years</option> | |
| </select>`; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COMPARE COMPANY MANAGEMENT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function removeCompare(c) { | |
| state.compareCompanies = state.compareCompanies.filter(x => x !== c); | |
| renderCtx(); | |
| } | |
| function openComparePicker() { | |
| const picker = document.getElementById('company-picker'); | |
| picker.classList.add('open'); | |
| document.getElementById('picker-search').focus(); | |
| picker._mode = 'compare'; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COMPANY PICKER | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function buildPicker() { | |
| renderPickerItems(state.companies); | |
| } | |
| function renderPickerItems(items) { | |
| const list = document.getElementById('picker-list'); | |
| list.innerHTML = items.map(c => | |
| `<div class="picker-item" onclick="selectFromPicker('${c}')"> | |
| <span>${c}</span> | |
| <span class="ticker">${state.tickers[c] || ''}</span> | |
| </div>` | |
| ).join(''); | |
| } | |
| function filterPicker(q) { | |
| const filtered = state.companies.filter(c => c.toLowerCase().includes(q.toLowerCase())); | |
| renderPickerItems(filtered); | |
| } | |
| function togglePicker() { | |
| const picker = document.getElementById('company-picker'); | |
| picker.classList.toggle('open'); | |
| picker._mode = 'chat'; | |
| if (picker.classList.contains('open')) { | |
| document.getElementById('picker-search').value = ''; | |
| document.getElementById('picker-search').focus(); | |
| renderPickerItems(state.companies); | |
| } | |
| } | |
| function selectFromPicker(company) { | |
| const picker = document.getElementById('company-picker'); | |
| if (picker._mode === 'compare') { | |
| if (!state.compareCompanies.includes(company)) { | |
| state.compareCompanies.push(company); | |
| } | |
| renderCtx(); | |
| } else { | |
| state.company = company; | |
| const sel = document.getElementById('company-sel'); | |
| if (sel) sel.value = company; | |
| // Also insert @company into input | |
| const inp = document.getElementById('query-input'); | |
| if (!inp.value.includes(company)) { | |
| inp.value = `@${company} ` + inp.value; | |
| } | |
| inp.focus(); | |
| } | |
| picker.classList.remove('open'); | |
| } | |
| // Close picker on outside click | |
| document.addEventListener('click', e => { | |
| const picker = document.getElementById('company-picker'); | |
| const atBtn = document.querySelector('.at-btn'); | |
| if (!picker.contains(e.target) && !atBtn.contains(e.target)) { | |
| picker.classList.remove('open'); | |
| } | |
| }); | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SUGGESTIONS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const SUGGESTIONS = [ | |
| { icon: 'π°', text: "What was TCS's revenue and net profit for FY2025?" }, | |
| { icon: 'π¦', text: "What is HDFC Bank's gross NPA ratio?" }, | |
| { icon: 'βοΈ', text: "Compare Infosys vs HCL Technologies on revenue growth" }, | |
| { icon: 'π', text: "Show KPI dashboard for Reliance Industries" }, | |
| ]; | |
| function renderSuggestions() { | |
| const grid = document.getElementById('suggestions'); | |
| grid.innerHTML = SUGGESTIONS.map(s => | |
| `<div class="sug-card" onclick="submitSuggestion('${s.text.replace(/'/g,"\\'")}')"> | |
| <span class="sug-icon">${s.icon}</span> | |
| <div class="sug-text">${s.text}</div> | |
| </div>` | |
| ).join(''); | |
| } | |
| function submitSuggestion(text) { | |
| const inp = document.getElementById('query-input'); | |
| inp.value = text; | |
| submitQuery(); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEXTAREA AUTO-RESIZE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function autoResize(el) { | |
| el.style.height = 'auto'; | |
| el.style.height = Math.min(el.scrollHeight, 180) + 'px'; | |
| } | |
| function handleKey(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| submitQuery(); | |
| } | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CONVERSATION MANAGEMENT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function newChat() { | |
| const res = await fetch('/api/conversations', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ mode: state.mode }), | |
| }); | |
| const data = await res.json(); | |
| state.conversationId = data.id; | |
| // Clear thread, show welcome | |
| document.getElementById('thread').innerHTML = ` | |
| <div id="welcome"> | |
| <div class="wlc-logo">π</div> | |
| <div class="wlc-title">FinRAG</div> | |
| <div class="wlc-sub">Ask anything about 17 major Indian companies.<br>Answers grounded in official BSE annual reports.</div> | |
| <div class="sug-grid" id="suggestions"></div> | |
| </div>`; | |
| renderSuggestions(); | |
| loadHistory(); | |
| } | |
| async function loadHistory() { | |
| const res = await fetch('/api/conversations'); | |
| const data = await res.json(); | |
| renderHistory(data.grouped || {}); | |
| } | |
| function renderHistory(grouped) { | |
| const list = document.getElementById('history-list'); | |
| let html = ''; | |
| const icons = { chat: 'π¬', compare: 'βοΈ', dashboard: 'π' }; | |
| for (const [group, convs] of Object.entries(grouped)) { | |
| html += `<div class="group-label">${group}</div>`; | |
| convs.forEach(c => { | |
| const active = c.id === state.conversationId ? 'active' : ''; | |
| const icon = icons[c.mode] || 'π¬'; | |
| html += `<div class="hist-item ${active}" onclick="loadConversation('${c.id}', '${c.mode}')"> | |
| <span class="icon">${icon}</span> | |
| <span class="title">${escHtml(c.title)}</span> | |
| <span class="del" onclick="event.stopPropagation();deleteConv('${c.id}')">π</span> | |
| </div>`; | |
| }); | |
| } | |
| list.innerHTML = html || '<div style="padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--text-dim);">No history yet</div>'; | |
| } | |
| async function loadConversation(cid, mode) { | |
| state.conversationId = cid; | |
| if (mode) setMode(mode); | |
| const res = await fetch(`/api/conversations/${cid}/messages`); | |
| const data = await res.json(); | |
| const thread = document.getElementById('thread'); | |
| thread.innerHTML = ''; | |
| for (const msg of data.messages) { | |
| if (msg.role === 'user') { | |
| appendUserMsg(msg.content); | |
| } else { | |
| const meta = msg.metadata || {}; | |
| const el = appendAssistantMsg(msg.content, false); | |
| if (meta.sources?.length) appendSources(el, meta.sources); | |
| if (meta.kpi_metrics) appendKPI(el, meta.kpi_metrics, meta.company || ''); | |
| } | |
| } | |
| thread.scrollTop = thread.scrollHeight; | |
| loadHistory(); | |
| } | |
| async function deleteConv(cid) { | |
| await fetch(`/api/conversations/${cid}`, { method: 'DELETE' }); | |
| if (state.conversationId === cid) { | |
| state.conversationId = null; | |
| newChat(); | |
| } else { | |
| loadHistory(); | |
| } | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SAVE MESSAGE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function saveMessage(role, content, metadata = null) { | |
| if (!state.conversationId) { | |
| const res = await fetch('/api/conversations', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ mode: state.mode }), | |
| }); | |
| const data = await res.json(); | |
| state.conversationId = data.id; | |
| } | |
| await fetch(`/api/conversations/${state.conversationId}/messages`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ role, content, metadata }), | |
| }); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SUBMIT QUERY | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function submitQuery() { | |
| const inp = document.getElementById('query-input'); | |
| const query = inp.value.trim(); | |
| if (!query || state.isStreaming) return; | |
| inp.value = ''; | |
| inp.style.height = 'auto'; | |
| // Hide welcome | |
| const welcome = document.getElementById('welcome'); | |
| if (welcome) welcome.remove(); | |
| // Get current selectors FIRST | |
| const companySel = document.getElementById('company-sel'); | |
| if (companySel) state.company = companySel.value; | |
| const fySel = document.getElementById('fy-sel'); | |
| if (fySel) state.fy = fySel.value; | |
| const ql = query.toLowerCase(); | |
| // Auto-detect compare/dashboard intent FIRST | |
| if (/compare|vs |versus|vs\./i.test(ql) && state.mode !== 'compare') setMode('compare'); | |
| else if (/dashboard|kpi|extract metric/i.test(ql) && state.mode !== 'dashboard') setMode('dashboard'); | |
| if (state.mode === 'compare') { | |
| const mentioned = state.companies.filter(c => ql.includes(c.toLowerCase()) || (state.tickers[c] && ql.includes(state.tickers[c].toLowerCase()))); | |
| if (mentioned.length > 0) { | |
| state.compareCompanies = []; | |
| mentioned.forEach(c => { | |
| if (!state.compareCompanies.includes(c)) state.compareCompanies.push(c); | |
| }); | |
| renderCtx(); | |
| } | |
| } else { | |
| // Single company detection for chat/dashboard | |
| const atMatch = query.match(/@(\w+)/); | |
| if (atMatch) { | |
| const mentioned = state.companies.find(c => c.toLowerCase() === atMatch[1].toLowerCase()); | |
| if (mentioned) state.company = mentioned; | |
| } else { | |
| for (const c of state.companies) { | |
| if (ql.includes(c.toLowerCase()) || (state.tickers[c] && ql.includes(state.tickers[c].toLowerCase()))) { | |
| state.company = c; | |
| break; | |
| } | |
| } | |
| } | |
| // Sync dropdown UI | |
| if (companySel && companySel.value !== state.company) { | |
| companySel.value = state.company; | |
| } | |
| } | |
| appendUserMsg(query); | |
| await saveMessage('user', query); | |
| loadHistory(); | |
| if (state.mode === 'dashboard') { | |
| await runDashboard(); | |
| } else if (state.mode === 'compare') { | |
| await runCompare(query); | |
| } else { | |
| await runChat(query); | |
| } | |
| loadHistory(); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CHAT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runChat(query) { | |
| setBusy(true); | |
| const spinEl = appendSpinner('Searching documentsβ¦'); | |
| let fullText = ''; | |
| let sources = []; | |
| const asst = createAssistantEl(); | |
| try { | |
| const res = await fetch('/api/chat', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query, company: state.company, fiscal_year: state.fy }), | |
| }); | |
| spinEl.remove(); | |
| const thread = document.getElementById('thread'); | |
| thread.appendChild(asst.wrap); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buf = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buf += decoder.decode(value, { stream: true }); | |
| const lines = buf.split('\n'); | |
| buf = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.startsWith('data: ')) continue; | |
| const evt = JSON.parse(line.slice(6)); | |
| if (evt.type === 'chunk') { | |
| fullText += evt.text; | |
| asst.content.innerHTML = marked.parse(fullText) + '<span class="cursor"></span>'; | |
| scrollThread(); | |
| } else if (evt.type === 'sources') { | |
| sources = evt.sources; | |
| } else if (evt.type === 'done') { | |
| asst.content.innerHTML = marked.parse(fullText); | |
| if (sources.length) appendSources(asst.content, sources); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| spinEl.remove(); | |
| appendAssistantMsg(`Error: ${e.message}`, false); | |
| } | |
| await saveMessage('assistant', fullText, { sources }); | |
| setBusy(false); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COMPARE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runCompare(query) { | |
| setBusy(true); | |
| const companies = state.compareCompanies.length >= 1 ? state.compareCompanies : [state.company]; | |
| const spinEl = appendSpinner(`Comparing ${companies.join(' vs ')}β¦`); | |
| let fullText = ''; | |
| let sources = []; | |
| const asst = createAssistantEl(); | |
| try { | |
| const res = await fetch('/api/compare', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query, companies, fiscal_year: state.fy }), | |
| }); | |
| spinEl.remove(); | |
| document.getElementById('thread').appendChild(asst.wrap); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buf = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buf += decoder.decode(value, { stream: true }); | |
| const lines = buf.split('\n'); | |
| buf = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.startsWith('data: ')) continue; | |
| const evt = JSON.parse(line.slice(6)); | |
| if (evt.type === 'chunk') { | |
| fullText += evt.text; | |
| asst.content.innerHTML = marked.parse(fullText) + '<span class="cursor"></span>'; | |
| scrollThread(); | |
| } else if (evt.type === 'sources') { | |
| sources = evt.sources; | |
| } else if (evt.type === 'done') { | |
| asst.content.innerHTML = marked.parse(fullText); | |
| if (sources.length) appendSources(asst.content, sources); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| spinEl.remove(); | |
| appendAssistantMsg(`Error: ${e.message}`, false); | |
| } | |
| await saveMessage('assistant', fullText, { sources }); | |
| setBusy(false); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DASHBOARD | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function extractDashboard() { | |
| const companySel = document.getElementById('company-sel'); | |
| if (companySel) state.company = companySel.value; | |
| const fySel = document.getElementById('fy-sel'); | |
| if (fySel) state.fy = fySel.value; | |
| const welcome = document.getElementById('welcome'); | |
| if (welcome) welcome.remove(); | |
| const query = `Show KPI dashboard for ${state.company} ${state.fy}`; | |
| appendUserMsg(query); | |
| await saveMessage('user', query); | |
| await runDashboard(); | |
| loadHistory(); | |
| } | |
| async function runDashboard() { | |
| setBusy(true); | |
| const companySel = document.getElementById('company-sel'); | |
| if (companySel) state.company = companySel.value; | |
| const spinEl = appendSpinner(`Extracting KPIs for ${state.company}β¦`); | |
| const asst = createAssistantEl(); | |
| try { | |
| const res = await fetch('/api/dashboard', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ company: state.company, fiscal_year: state.fy }), | |
| }); | |
| const data = await res.json(); | |
| const metrics = data.metrics || {}; | |
| const fyLabel = data.fiscal_year || state.fy; | |
| const ticker = state.tickers[state.company] || ''; | |
| spinEl.remove(); | |
| const thread = document.getElementById('thread'); | |
| thread.appendChild(asst.wrap); | |
| asst.content.innerHTML = `<strong>${state.company}</strong> (${ticker}) β KPI Summary <span style="color:var(--accent2)">${fyLabel}</span>`; | |
| appendKPI(asst.content, metrics, state.company); | |
| await saveMessage('assistant', | |
| `${state.company} KPI Dashboard β ${fyLabel}`, | |
| { kpi_metrics: metrics, company: state.company, fy: fyLabel } | |
| ); | |
| } catch (e) { | |
| spinEl.remove(); | |
| appendAssistantMsg(`Error: ${e.message}`, false); | |
| } | |
| setBusy(false); | |
| scrollThread(); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DOM HELPERS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function appendUserMsg(text) { | |
| const thread = document.getElementById('thread'); | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'msg-wrap'; | |
| wrap.innerHTML = `<div class="msg-user"><div class="bubble">${escHtml(text)}</div></div>`; | |
| thread.appendChild(wrap); | |
| scrollThread(); | |
| } | |
| function createAssistantEl() { | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'msg-wrap'; | |
| const content = document.createElement('div'); | |
| content.className = 'asst-content'; | |
| wrap.innerHTML = ` | |
| <div class="msg-asst"> | |
| <div class="asst-avatar">π</div> | |
| <div class="asst-body"> | |
| <div class="asst-name">FinRAG</div> | |
| </div> | |
| </div>`; | |
| wrap.querySelector('.asst-body').appendChild(content); | |
| return { wrap, content }; | |
| } | |
| function appendAssistantMsg(markdown, streaming = false) { | |
| const thread = document.getElementById('thread'); | |
| const { wrap, content } = createAssistantEl(); | |
| content.innerHTML = marked.parse(markdown); | |
| thread.appendChild(wrap); | |
| scrollThread(); | |
| return content; | |
| } | |
| function appendSpinner(text) { | |
| const thread = document.getElementById('thread'); | |
| const el = document.createElement('div'); | |
| el.style.cssText = 'max-width:780px;margin:0.5rem auto;padding:0 1.5rem;'; | |
| el.innerHTML = `<div class="spin-row"><div class="spinner"></div><span>${escHtml(text)}</span></div>`; | |
| thread.appendChild(el); | |
| scrollThread(); | |
| return el; | |
| } | |
| function appendSources(container, sources) { | |
| if (!sources?.length) return; | |
| const div = document.createElement('div'); | |
| div.className = 'sources-bar'; | |
| sources.forEach(s => { | |
| const chip = document.createElement('a'); | |
| chip.className = 'src-chip' + (s.is_table ? ' tbl' : ''); | |
| chip.textContent = `${s.company || ''} ${s.fiscal_year || ''} Pg.${s.page}${s.is_table ? ' [TABLE]' : ''}`.trim(); | |
| chip.title = s.file || ''; | |
| if (s.file) { | |
| chip.href = `/raw_pdfs/${encodeURIComponent(s.file)}#page=${s.page}`; | |
| chip.target = '_blank'; | |
| } | |
| div.appendChild(chip); | |
| }); | |
| container.appendChild(div); | |
| } | |
| function appendKPI(container, metrics, company) { | |
| const fmt = (v, type) => { | |
| if (v === null || v === undefined) return null; | |
| if (type === 'crore') return v >= 100000 ? `βΉ${(v/100000).toFixed(1)}L Cr` : `βΉ${Math.round(v).toLocaleString('en-IN')} Cr`; | |
| if (type === 'pct') return `${v.toFixed(2)}%`; | |
| if (type === 'inr') return `βΉ${v.toFixed(2)}`; | |
| return String(v); | |
| }; | |
| const rows = [ | |
| { title: 'Revenue & Profitability', items: [ | |
| ['Revenue', metrics.revenue_crore, fmt(metrics.revenue_crore, 'crore')], | |
| ['Net Profit', metrics.net_profit_crore, fmt(metrics.net_profit_crore, 'crore')], | |
| ['Net Margin', metrics.net_profit_margin_pct, fmt(metrics.net_profit_margin_pct, 'pct')], | |
| ['EBITDA Margin',metrics.ebitda_margin_pct, fmt(metrics.ebitda_margin_pct, 'pct')], | |
| ]}, | |
| { title: 'Balance Sheet', items: [ | |
| ['Total Assets', metrics.total_assets_crore, fmt(metrics.total_assets_crore, 'crore')], | |
| ['Equity', metrics.equity_crore, fmt(metrics.equity_crore, 'crore')], | |
| ['EPS', metrics.eps_inr, fmt(metrics.eps_inr, 'inr')], | |
| ['ROE', metrics.roe_pct, fmt(metrics.roe_pct, 'pct')], | |
| ]}, | |
| ]; | |
| const hasBanking = metrics.npa_gross_pct || metrics.npa_net_pct || metrics.nim_pct; | |
| if (hasBanking) rows.push({ title: 'Banking Metrics', items: [ | |
| ['Gross NPA', metrics.npa_gross_pct, fmt(metrics.npa_gross_pct, 'pct')], | |
| ['Net NPA', metrics.npa_net_pct, fmt(metrics.npa_net_pct, 'pct')], | |
| ['NIM', metrics.nim_pct, fmt(metrics.nim_pct, 'pct')], | |
| ['EBITDA', metrics.ebitda_crore, fmt(metrics.ebitda_crore, 'crore')], | |
| ]}); | |
| const section = document.createElement('div'); | |
| section.className = 'kpi-section'; | |
| const colorMetrics = ['Net Margin', 'EBITDA Margin', 'EPS', 'ROE', 'NIM']; | |
| const inverseColorMetrics = ['Gross NPA', 'Net NPA']; | |
| rows.forEach(row => { | |
| const validItems = row.items.filter(([lbl, raw, val]) => val !== null); | |
| if (validItems.length === 0) return; // Skip section if all metrics are N/A | |
| section.innerHTML += `<div class="kpi-section-title">${row.title}</div>`; | |
| const grid = document.createElement('div'); | |
| grid.className = 'kpi-grid'; | |
| validItems.forEach(([lbl, raw, val]) => { | |
| let colorClass = ''; | |
| if (colorMetrics.includes(lbl) && raw !== null) { | |
| colorClass = raw > 0 ? ' text-green' : (raw < 0 ? ' text-red' : ''); | |
| } else if (inverseColorMetrics.includes(lbl) && raw !== null) { | |
| colorClass = ' text-red'; // NPA is bad | |
| } | |
| const card = document.createElement('div'); | |
| card.className = 'kpi-card has-val'; | |
| card.innerHTML = `<div class="val${colorClass}">${val}</div><div class="lbl">${lbl}</div>`; | |
| grid.appendChild(card); | |
| }); | |
| section.appendChild(grid); | |
| }); | |
| container.appendChild(section); | |
| // Charts | |
| const renderChart = (id, title, dataMap) => { | |
| const filtered = Object.entries(dataMap).filter(([,v]) => v != null); | |
| if (filtered.length < 1) return; | |
| const chartWrap = document.createElement('div'); | |
| chartWrap.className = 'chart-wrap'; | |
| chartWrap.innerHTML = `<div id="${id}"></div>`; | |
| container.appendChild(chartWrap); | |
| setTimeout(() => { | |
| Plotly.newPlot(id, | |
| [{ type: 'bar', orientation: 'h', | |
| y: filtered.map(([k]) => k), | |
| x: filtered.map(([,v]) => v), | |
| marker: { color: ['#3b82f6','#06b6d4','#8b5cf6'] }, | |
| text: filtered.map(([,v]) => `βΉ${(v).toLocaleString('en-IN')} Cr`), | |
| textposition: 'outside', textfont: { color: '#e2e8f0', size: 11 }, | |
| }], | |
| { paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(255,255,255,0.02)', | |
| font: { color: '#94a3b8', family: 'Inter' }, | |
| xaxis: { gridcolor: 'rgba(255,255,255,0.05)', tickfont: { color: '#64748b' } }, | |
| yaxis: { tickfont: { color: '#e2e8f0', size: 12 }, automargin: true }, | |
| margin: { t: 35, b: 10, l: 10, r: 80 }, height: filtered.length === 1 ? 90 : 160, | |
| title: { text: title, font: { color: '#64748b', size: 11 } }, | |
| }, | |
| { displayModeBar: false, responsive: true } | |
| ); | |
| }, 100); | |
| }; | |
| const stamp = Date.now(); | |
| renderChart('chart-pl-' + stamp, `${company} β P&L Overview`, { | |
| 'Revenue': metrics.revenue_crore, | |
| 'EBITDA': metrics.ebitda_crore, | |
| 'Net Profit': metrics.net_profit_crore, | |
| }); | |
| renderChart('chart-bs-' + stamp, `${company} β Balance Sheet`, { | |
| 'Total Assets': metrics.total_assets_crore, | |
| 'Equity': metrics.equity_crore, | |
| }); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UTILS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function scrollThread() { | |
| const t = document.getElementById('thread'); | |
| t.scrollTop = t.scrollHeight; | |
| } | |
| function setBusy(busy) { | |
| state.isStreaming = busy; | |
| document.getElementById('send-btn').disabled = busy; | |
| const eb = document.getElementById('extract-btn'); | |
| if (eb) eb.disabled = busy; | |
| } | |
| function escHtml(s) { | |
| return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| /* Configure marked */ | |
| marked.setOptions({ breaks: true, gfm: true }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |