Download index.html from junaid17/FinAI: direct link, hf CLI and curl.
- Browser
- Download file 45 kB
-
https://huggingface.co/spaces/junaid17/FinAI/resolve/main/index.html
- Command line
-
hf download hf://spaces/junaid17/FinAI/index.html
-
curl -L -o index.html https://huggingface.co/spaces/junaid17/FinAI/resolve/main/index.html
45 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FinAI — Financial Intelligence</title> | |
| <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"> | |
| <style> | |
| :root { | |
| --bg: #050505; | |
| --bg-elevated: #0a0a0a; | |
| --bg-card: #0f0f0f; | |
| --bg-hover: #161616; | |
| --bg-active: #1a1a1a; | |
| --border: #1f1f1f; | |
| --border-light: #2a2a2a; | |
| --border-hover: #333333; | |
| --text: #f5f5f5; | |
| --text-secondary: #a0a0a0; | |
| --text-muted: #666666; | |
| --text-dim: #444444; | |
| --accent: #e8e8e8; | |
| --accent-dim: #999999; | |
| --radius-sm: 16px; | |
| --radius: 20px; | |
| --radius-lg: 24px; | |
| --radius-pill: 999px; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| height: 100vh; | |
| overflow: hidden; | |
| line-height: 1.5; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| ::-webkit-scrollbar { width: 4px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } | |
| @keyframes fadeInScale { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } } | |
| @keyframes slideInRight { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } } | |
| @keyframes slideInLeft { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } } | |
| @keyframes pulse { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } } | |
| @keyframes breathe { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.05); opacity: 1; } } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| @keyframes typingDot { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } | |
| @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } | |
| @keyframes progressFill { from { width: 0%; } to { width: 100%; } } | |
| @keyframes cursorBlink { 0%,100% { opacity: 1; } 50% { opacity: 0; } } | |
| @keyframes ripple { 0% { transform: scale(0); opacity: 0.5; } 100% { transform: scale(4); opacity: 0; } } | |
| @keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } | |
| .app { display: flex; height: 100vh; width: 100vw; } | |
| /* ===== SIDEBAR ===== */ | |
| .sidebar { | |
| width: 300px; | |
| background: var(--bg-elevated); | |
| border-right: 1px solid var(--border); | |
| display: flex; flex-direction: column; | |
| padding: 20px; | |
| gap: 20px; | |
| overflow-y: auto; | |
| transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| position: relative; z-index: 100; | |
| } | |
| .sidebar-logo { | |
| display: flex; align-items: center; gap: 12px; | |
| padding: 8px 4px; | |
| } | |
| .logo-ring { | |
| width: 36px; height: 36px; | |
| border-radius: var(--radius-sm); | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border-light); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 16px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .logo-ring::after { | |
| content: ''; position: absolute; inset: -2px; | |
| border-radius: inherit; | |
| background: conic-gradient(from 0deg, transparent, var(--text-dim), transparent); | |
| opacity: 0.3; | |
| animation: spin 8s linear infinite; | |
| z-index: -1; | |
| } | |
| .logo-text { | |
| font-size: 18px; font-weight: 700; | |
| letter-spacing: -0.4px; | |
| color: var(--text); | |
| } | |
| .new-chat { | |
| display: flex; align-items: center; justify-content: center; | |
| gap: 8px; padding: 12px; | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| font-size: 13px; font-weight: 500; | |
| font-family: inherit; cursor: pointer; | |
| transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .new-chat:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--border-hover); | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.4); | |
| } | |
| .new-chat:active { transform: scale(0.98); } | |
| .section-label { | |
| font-size: 10px; font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: var(--text-dim); | |
| padding: 0 6px; | |
| margin-bottom: 6px; | |
| } | |
| .recent-list { display: flex; flex-direction: column; gap: 2px; } | |
| .recent-item { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 12px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| position: relative; | |
| } | |
| .recent-item:hover { background: var(--bg-hover); } | |
| .recent-item.active { background: var(--bg-active); } | |
| .recent-item.active::before { | |
| content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); | |
| width: 3px; height: 14px; background: var(--text-secondary); border-radius: 2px; | |
| } | |
| .recent-dot { | |
| width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--border-light); flex-shrink: 0; | |
| } | |
| .recent-item.active .recent-dot { background: var(--text-secondary); } | |
| .recent-title { | |
| font-size: 13px; font-weight: 500; | |
| color: var(--text-secondary); | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| } | |
| .recent-time { font-size: 11px; color: var(--text-dim); margin-top: 1px; } | |
| /* Upload Area */ | |
| .upload-wrap { display: flex; flex-direction: column; gap: 10px; } | |
| .upload-zone { | |
| border: 1.5px dashed var(--border-light); | |
| border-radius: var(--radius-sm); | |
| padding: 24px 16px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: var(--bg-card); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .upload-zone:hover { | |
| border-color: var(--border-hover); | |
| background: var(--bg-hover); | |
| } | |
| .upload-zone.dragover { | |
| border-color: var(--text-muted); | |
| background: rgba(255,255,255,0.02); | |
| } | |
| .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } | |
| .upload-zone-icon { font-size: 28px; margin-bottom: 8px; display: block; filter: grayscale(1); opacity: 0.7; } | |
| .upload-zone-text { font-size: 13px; color: var(--text-secondary); font-weight: 500; } | |
| .upload-zone-hint { font-size: 11px; color: var(--text-dim); margin-top: 3px; } | |
| /* Upload Progress */ | |
| .upload-progress { | |
| display: none; | |
| flex-direction: column; | |
| gap: 8px; | |
| padding: 12px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| animation: fadeInScale 0.3s ease; | |
| } | |
| .upload-progress.show { display: flex; } | |
| .progress-track { | |
| height: 3px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--text-dim), var(--text-secondary), var(--text-dim)); | |
| background-size: 200% 100%; | |
| border-radius: 3px; | |
| width: 0%; | |
| transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| animation: shimmer 2s linear infinite; | |
| } | |
| .progress-info { | |
| display: flex; justify-content: space-between; align-items: center; | |
| font-size: 11px; | |
| } | |
| .progress-step { | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| .progress-step .spinner { | |
| width: 12px; height: 12px; | |
| border: 1.5px solid transparent; | |
| border-top-color: var(--text-secondary); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| .progress-pct { color: var(--text-dim); font-family: 'JetBrains Mono', monospace; font-size: 10px; } | |
| .upload-done { | |
| display: none; | |
| padding: 12px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| animation: fadeInScale 0.3s ease; | |
| } | |
| .upload-done.show { display: flex; align-items: center; gap: 10px; } | |
| .upload-done-icon { | |
| width: 28px; height: 28px; | |
| border-radius: 50%; | |
| background: var(--bg-hover); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 13px; | |
| flex-shrink: 0; | |
| } | |
| .upload-done-text { font-size: 12px; color: var(--text-secondary); font-weight: 500; } | |
| .upload-done-meta { font-size: 10px; color: var(--text-dim); margin-top: 2px; } | |
| .upload-btn { | |
| padding: 10px; | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| font-size: 13px; font-weight: 500; | |
| font-family: inherit; cursor: pointer; | |
| transition: all 0.25s ease; | |
| width: 100%; | |
| } | |
| .upload-btn:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--border-hover); | |
| } | |
| .upload-btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .reset-btn { | |
| padding: 10px; | |
| background: transparent; | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text-muted); | |
| font-size: 12px; font-weight: 500; | |
| font-family: inherit; cursor: pointer; | |
| transition: all 0.2s; | |
| width: 100%; | |
| display: flex; align-items: center; justify-content: center; gap: 6px; | |
| } | |
| .reset-btn:hover { | |
| border-color: rgba(239,68,68,0.3); | |
| color: #ef4444; | |
| background: rgba(239,68,68,0.03); | |
| } | |
| /* Session */ | |
| .session-box { | |
| margin-top: auto; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--border); | |
| display: flex; flex-direction: column; gap: 10px; | |
| } | |
| .session-label { | |
| display: flex; justify-content: space-between; align-items: center; | |
| font-size: 10px; color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| } | |
| .session-id { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; color: var(--text-dim); | |
| word-break: break-all; | |
| padding: 8px 10px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| } | |
| .copy-mini { | |
| background: none; border: none; | |
| color: var(--text-dim); cursor: pointer; | |
| font-size: 10px; padding: 2px 6px; | |
| border-radius: 4px; transition: all 0.2s; | |
| } | |
| .copy-mini:hover { color: var(--text-secondary); background: var(--bg-hover); } | |
| .version { font-size: 10px; color: var(--text-dim); text-align: center; } | |
| /* ===== MAIN ===== */ | |
| .main { | |
| flex: 1; | |
| display: flex; flex-direction: column; | |
| position: relative; overflow: hidden; | |
| background: var(--bg); | |
| } | |
| /* Empty State */ | |
| .empty { | |
| flex: 1; | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| gap: 24px; | |
| padding: 40px 24px; | |
| animation: fadeInScale 0.6s ease; | |
| } | |
| .empty-orb { | |
| width: 72px; height: 72px; | |
| border-radius: var(--radius-lg); | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 28px; | |
| position: relative; | |
| animation: breathe 4s ease-in-out infinite; | |
| } | |
| .empty-orb::before { | |
| content: ''; position: absolute; inset: -4px; | |
| border-radius: inherit; | |
| background: conic-gradient(from 0deg, transparent, var(--border-light), transparent); | |
| opacity: 0.2; | |
| animation: spin 12s linear infinite; | |
| z-index: -1; | |
| } | |
| .empty-title { | |
| font-size: 32px; font-weight: 700; | |
| letter-spacing: -1px; | |
| color: var(--text); | |
| } | |
| .empty-desc { | |
| font-size: 14px; color: var(--text-muted); | |
| text-align: center; max-width: 400px; line-height: 1.7; | |
| } | |
| .empty-chips { | |
| display: flex; flex-wrap: wrap; | |
| justify-content: center; gap: 8px; | |
| max-width: 600px; margin-top: 4px; | |
| } | |
| .chip { | |
| padding: 8px 16px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-pill); | |
| font-size: 12px; color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); | |
| font-weight: 500; | |
| } | |
| .chip:hover { | |
| border-color: var(--border-hover); | |
| color: var(--text); | |
| background: var(--bg-hover); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.3); | |
| } | |
| /* Messages */ | |
| .messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 32px 24px 16px; | |
| display: flex; flex-direction: column; | |
| gap: 28px; | |
| scroll-behavior: smooth; | |
| } | |
| .msg { | |
| display: flex; gap: 14px; | |
| max-width: 780px; | |
| width: 100%; | |
| align-self: center; | |
| animation: fadeIn 0.35s ease-out; | |
| } | |
| .msg-avatar { | |
| width: 32px; height: 32px; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; | |
| font-size: 13px; | |
| position: relative; | |
| } | |
| .msg-avatar.user { | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border-light); | |
| color: var(--text-secondary); | |
| } | |
| .msg-avatar.assistant { | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border-light); | |
| color: var(--text); | |
| } | |
| .msg-avatar::after { | |
| content: ''; position: absolute; bottom: -2px; right: -2px; | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--bg); border: 2px solid var(--bg); | |
| } | |
| .msg-avatar.user::after { background: var(--text-dim); } | |
| .msg-avatar.assistant::after { background: var(--text-secondary); } | |
| .msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } | |
| .msg-head { | |
| display: flex; align-items: center; gap: 8px; | |
| font-size: 12px; | |
| } | |
| .msg-name { font-weight: 600; color: var(--text); } | |
| .msg-time { color: var(--text-dim); font-weight: 400; font-size: 11px; } | |
| .msg-text { | |
| font-size: 14.5px; line-height: 1.75; | |
| color: var(--text-secondary); | |
| word-wrap: break-word; | |
| padding: 2px 0; | |
| } | |
| .msg-text strong { color: var(--text); font-weight: 600; } | |
| .msg-text em { color: var(--text); font-style: italic; } | |
| .msg-text code { | |
| background: var(--bg-elevated); | |
| padding: 2px 6px; | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; color: var(--text); | |
| border: 1px solid var(--border); | |
| } | |
| .msg-text p { margin: 0 0 10px 0; } | |
| .msg-text p:last-child { margin-bottom: 0; } | |
| /* Typing */ | |
| .typing { | |
| display: flex; gap: 4px; align-items: center; | |
| padding: 6px 0; | |
| } | |
| .typing span { | |
| width: 5px; height: 5px; | |
| background: var(--text-dim); | |
| border-radius: 50%; | |
| animation: typingDot 1.4s ease-in-out infinite; | |
| } | |
| .typing span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing span:nth-child(3) { animation-delay: 0.4s; } | |
| /* Cursor */ | |
| .stream-cursor { | |
| display: inline-block; | |
| width: 2px; height: 1.1em; | |
| background: var(--text-secondary); | |
| margin-left: 2px; | |
| vertical-align: text-bottom; | |
| animation: cursorBlink 1s step-end infinite; | |
| } | |
| /* RAG Card */ | |
| .rag { | |
| margin-top: 10px; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| animation: fadeInScale 0.35s ease; | |
| } | |
| .rag-toggle { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 14px; | |
| cursor: pointer; | |
| user-select: none; | |
| transition: background 0.15s; | |
| } | |
| .rag-toggle:hover { background: var(--bg-hover); } | |
| .rag-badge { | |
| display: inline-flex; align-items: center; gap: 4px; | |
| padding: 3px 10px; border-radius: var(--radius-pill); | |
| font-size: 10px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 0.8px; | |
| } | |
| .rag-badge.rag-active { | |
| background: var(--bg-hover); | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border-light); | |
| } | |
| .rag-badge.rag-direct { | |
| background: var(--bg-hover); | |
| color: var(--text-dim); | |
| border: 1px solid var(--border); | |
| } | |
| .rag-label { font-size: 12px; color: var(--text-muted); font-weight: 500; } | |
| .rag-arrow { margin-left: auto; font-size: 10px; color: var(--text-dim); transition: transform 0.25s ease; } | |
| .rag-toggle.open .rag-arrow { transform: rotate(180deg); } | |
| .rag-body { | |
| display: none; | |
| padding: 0 14px 12px; | |
| flex-direction: column; gap: 6px; | |
| } | |
| .rag-body.open { display: flex; } | |
| .source { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 12px; | |
| background: var(--bg); | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--border); | |
| transition: all 0.2s ease; | |
| } | |
| .source:hover { | |
| border-color: var(--border-hover); | |
| background: var(--bg-card); | |
| transform: translateX(3px); | |
| } | |
| .source-icon { | |
| width: 28px; height: 28px; | |
| background: var(--bg-elevated); | |
| border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 12px; flex-shrink: 0; | |
| border: 1px solid var(--border); | |
| } | |
| .source-info { flex: 1; min-width: 0; } | |
| .source-name { | |
| font-size: 12px; font-weight: 500; | |
| color: var(--text-secondary); | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| } | |
| .source-meta { font-size: 10px; color: var(--text-dim); margin-top: 1px; } | |
| .source-score { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; color: var(--text-secondary); | |
| background: var(--bg-elevated); | |
| padding: 3px 8px; border-radius: 6px; | |
| border: 1px solid var(--border); | |
| } | |
| .rag-footer { | |
| padding-top: 8px; margin-top: 4px; | |
| border-top: 1px solid var(--border); | |
| font-size: 10px; color: var(--text-dim); | |
| font-family: 'JetBrains Mono', monospace; | |
| display: flex; justify-content: space-between; | |
| } | |
| /* Performance */ | |
| .perf { | |
| display: flex; align-items: center; gap: 14px; | |
| padding: 4px 0; | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .perf-item { display: flex; align-items: center; gap: 5px; } | |
| .perf-dot { | |
| width: 4px; height: 4px; border-radius: 50%; | |
| background: var(--text-dim); | |
| } | |
| /* Input */ | |
| .input-area { | |
| padding: 16px 24px 28px; | |
| display: flex; flex-direction: column; | |
| align-items: center; gap: 10px; | |
| position: relative; | |
| } | |
| .input-box { | |
| display: flex; align-items: flex-end; gap: 10px; | |
| width: 100%; max-width: 720px; | |
| background: var(--bg-elevated); | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 6px 6px 6px 18px; | |
| transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .input-box:focus-within { | |
| border-color: var(--border-hover); | |
| box-shadow: 0 0 0 4px rgba(255,255,255,0.03), 0 8px 32px rgba(0,0,0,0.4); | |
| } | |
| .input-field { | |
| flex: 1; | |
| background: transparent; border: none; | |
| color: var(--text); font-family: inherit; | |
| font-size: 14.5px; padding: 10px 0; | |
| resize: none; outline: none; | |
| max-height: 160px; min-height: 24px; | |
| line-height: 1.6; | |
| } | |
| .input-field::placeholder { color: var(--text-dim); } | |
| .send-btn { | |
| width: 32px; height: 32px; | |
| border-radius: 50%; | |
| background: var(--bg-card); | |
| border: 1.5px solid var(--border); | |
| color: var(--text-secondary); | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); | |
| flex-shrink: 0; margin-bottom: 2px; | |
| } | |
| .send-btn:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--border-hover); | |
| color: var(--text); | |
| transform: scale(1.08); | |
| } | |
| .send-btn:active { transform: scale(0.95); } | |
| .send-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; } | |
| .send-btn svg { width: 15px; height: 15px; } | |
| .input-hint { | |
| font-size: 11px; color: var(--text-dim); | |
| text-align: center; | |
| } | |
| /* Status */ | |
| .status-bar { | |
| position: absolute; bottom: 0; left: 0; right: 0; | |
| padding: 6px 24px; | |
| display: flex; align-items: center; gap: 8px; | |
| font-size: 11px; color: var(--text-dim); | |
| pointer-events: none; | |
| } | |
| .status-pulse { | |
| width: 5px; height: 5px; | |
| background: var(--text-dim); | |
| border-radius: 50%; | |
| } | |
| .status-pulse.active { | |
| background: var(--text-secondary); | |
| animation: pulse 2s ease-in-out infinite; | |
| box-shadow: 0 0 6px rgba(160,160,160,0.3); | |
| } | |
| /* Toast */ | |
| .toasts { | |
| position: fixed; top: 16px; right: 16px; | |
| z-index: 1000; | |
| display: flex; flex-direction: column; gap: 8px; | |
| } | |
| .toast { | |
| padding: 12px 18px; border-radius: var(--radius-sm); | |
| font-size: 13px; font-weight: 500; | |
| display: flex; align-items: center; gap: 10px; | |
| animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1); | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.5); | |
| backdrop-filter: blur(16px); | |
| max-width: 380px; | |
| border: 1px solid var(--border); | |
| } | |
| .toast.ok { background: var(--bg-card); color: var(--text); } | |
| .toast.err { background: var(--bg-card); color: #ef4444; } | |
| .toast.info { background: var(--bg-card); color: var(--text-secondary); } | |
| /* Mobile */ | |
| .mobile-toggle { | |
| display: none; | |
| position: absolute; left: 16px; top: 14px; | |
| width: 36px; height: 36px; | |
| border-radius: var(--radius-sm); | |
| border: 1.5px solid var(--border); | |
| background: var(--bg-elevated); | |
| color: var(--text-secondary); | |
| align-items: center; justify-content: center; | |
| cursor: pointer; font-size: 16px; z-index: 60; | |
| } | |
| .sidebar-overlay { | |
| display: none; position: fixed; inset: 0; | |
| background: rgba(0,0,0,0.6); | |
| z-index: 99; backdrop-filter: blur(4px); | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { position: fixed; left: 0; top: 0; height: 100vh; transform: translateX(-100%); width: 280px; } | |
| .sidebar.open { transform: translateX(0); } | |
| .sidebar-overlay.show { display: block; } | |
| .mobile-toggle { display: flex; } | |
| .messages { padding: 56px 16px 16px; } | |
| .msg { max-width: 100%; } | |
| .input-area { padding: 12px 16px 20px; } | |
| .empty-title { font-size: 26px; } | |
| } | |
| /* Chat header */ | |
| .chat-header { | |
| padding: 14px 24px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: center; | |
| position: relative; | |
| background: rgba(5,5,5,0.8); | |
| backdrop-filter: blur(16px); | |
| } | |
| .chat-header-text { | |
| font-size: 13px; font-weight: 600; | |
| color: var(--text-muted); | |
| letter-spacing: 0.5px; | |
| } | |
| .spinner { | |
| width: 14px; height: 14px; | |
| border: 1.5px solid transparent; | |
| border-top-color: currentColor; | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| </style> | |
| <base target="_blank"> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-logo"> | |
| <div class="logo-ring">💹</div> | |
| <div class="logo-text">FinAI</div> | |
| </div> | |
| <button class="new-chat" id="newChatBtn"> | |
| <span style="font-size:16px;">+</span> New Chat | |
| </button> | |
| <div> | |
| <div class="section-label">Recent Chats</div> | |
| <div class="recent-list" id="recentList"></div> | |
| </div> | |
| <div class="upload-wrap"> | |
| <div class="section-label">Upload Document</div> | |
| <div class="upload-zone" id="uploadZone"> | |
| <input type="file" id="fileInput" accept=".pdf" /> | |
| <span class="upload-zone-icon">📄</span> | |
| <div class="upload-zone-text" id="uzText">Drop PDF or click to browse</div> | |
| <div class="upload-zone-hint" id="uzHint">Financial reports, statements, tax docs</div> | |
| </div> | |
| <div class="upload-progress" id="uploadProgress"> | |
| <div class="progress-track"><div class="progress-fill" id="progressFill"></div></div> | |
| <div class="progress-info"> | |
| <span class="progress-step" id="progressStep"><div class="spinner"></div> Uploading PDF...</span> | |
| <span class="progress-pct" id="progressPct">0%</span> | |
| </div> | |
| </div> | |
| <div class="upload-done" id="uploadDone"> | |
| <div class="upload-done-icon">✓</div> | |
| <div> | |
| <div class="upload-done-text" id="udText">Document uploaded</div> | |
| <div class="upload-done-meta" id="udMeta">Ready for queries</div> | |
| </div> | |
| </div> | |
| <button class="upload-btn" id="uploadBtn">Upload Document</button> | |
| </div> | |
| <div> | |
| <div class="section-label">Danger Zone</div> | |
| <button class="reset-btn" id="resetBtn"> | |
| <span>🗑</span> Delete All Documents | |
| </button> | |
| </div> | |
| <div class="session-box"> | |
| <div class="session-label"> | |
| <span>Session ID</span> | |
| <button class="copy-mini" id="copyThread">Copy</button> | |
| </div> | |
| <div class="session-id" id="threadDisplay"></div> | |
| <div class="version">FinAI v1.0 · Qwen + RAG</div> | |
| </div> | |
| </aside> | |
| <div class="sidebar-overlay" id="sidebarOverlay"></div> | |
| <!-- Main --> | |
| <main class="main" id="main"> | |
| <button class="mobile-toggle" id="mobileToggle">☰</button> | |
| <header class="chat-header" id="chatHeader" style="display:none;"> | |
| <div class="chat-header-text">Conversation</div> | |
| </header> | |
| <div class="messages" id="messages"> | |
| <div class="empty" id="emptyState"> | |
| <div class="empty-orb">💹</div> | |
| <div class="empty-title">FinAI</div> | |
| <div class="empty-desc"> | |
| Your intelligent financial assistant. Upload a document and ask about dividends, | |
| taxes, retirement accounts, business valuation, or market analysis. | |
| </div> | |
| <div class="empty-chips" id="emptyChips"> | |
| <div class="chip" data-q="What are the risks of dividend-yielding stocks?">Dividend risks</div> | |
| <div class="chip" data-q="15-year vs 30-year mortgage paid in 15 — what's the difference?">Mortgage comparison</div> | |
| <div class="chip" data-q="How do taxes work for a new LLC business owner?">LLC taxes</div> | |
| <div class="chip" data-q="Can I have both a 401(k) and a SEP IRA?">Retirement accounts</div> | |
| <div class="chip" data-q="How to value a small business before investing?">Business valuation</div> | |
| <div class="chip" data-q="Why do companies have fiscal years different from calendar years?">Fiscal years</div> | |
| <div class="chip" data-q="What's wrong with taking money from your own business?">Owner withdrawals</div> | |
| <div class="chip" data-q="Is there a candlestick pattern that guarantees profit?">Trading patterns</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <div class="input-box"> | |
| <textarea class="input-field" id="inputField" rows="1" placeholder="Ask a finance question..."></textarea> | |
| <button class="send-btn" id="sendBtn"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="22" y1="2" x2="11" y2="13"></line> | |
| <polygon points="22,2 15,22 11,13 2,9 22,2"></polygon> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="input-hint">Enter to send · Shift+Enter for new line</div> | |
| </div> | |
| <div class="status-bar"> | |
| <span class="status-pulse" id="statusDot"></span> | |
| <span id="statusText">Ready</span> | |
| </div> | |
| </main> | |
| </div> | |
| <div class="toasts" id="toasts"></div> | |
| <script> | |
| // ================= CONFIG ================= | |
| const API_BASE = "http://127.0.0.1:8000"; | |
| // ================= STATE ================= | |
| let threadId = localStorage.getItem("finai_tid") || generateId(); | |
| let messages = JSON.parse(localStorage.getItem("finai_msgs") || "[]"); | |
| let isStreaming = false; | |
| let currentFile = null; | |
| let streamStart = 0; | |
| let tokCount = 0; | |
| localStorage.setItem("finai_tid", threadId); | |
| // ================= DOM ================= | |
| const sidebar = document.getElementById("sidebar"); | |
| const sidebarOverlay = document.getElementById("sidebarOverlay"); | |
| const mobileToggle = document.getElementById("mobileToggle"); | |
| const messagesEl = document.getElementById("messages"); | |
| const emptyState = document.getElementById("emptyState"); | |
| const inputField = document.getElementById("inputField"); | |
| const sendBtn = document.getElementById("sendBtn"); | |
| const uploadZone = document.getElementById("uploadZone"); | |
| const fileInput = document.getElementById("fileInput"); | |
| const uploadBtn = document.getElementById("uploadBtn"); | |
| const uploadProgress = document.getElementById("uploadProgress"); | |
| const progressFill = document.getElementById("progressFill"); | |
| const progressStep = document.getElementById("progressStep"); | |
| const progressPct = document.getElementById("progressPct"); | |
| const uploadDone = document.getElementById("uploadDone"); | |
| const udText = document.getElementById("udText"); | |
| const udMeta = document.getElementById("udMeta"); | |
| const uzText = document.getElementById("uzText"); | |
| const uzHint = document.getElementById("uzHint"); | |
| const resetBtn = document.getElementById("resetBtn"); | |
| const newChatBtn = document.getElementById("newChatBtn"); | |
| const threadDisplay = document.getElementById("threadDisplay"); | |
| const copyThread = document.getElementById("copyThread"); | |
| const toasts = document.getElementById("toasts"); | |
| const chatHeader = document.getElementById("chatHeader"); | |
| const statusDot = document.getElementById("statusDot"); | |
| const statusText = document.getElementById("statusText"); | |
| const recentList = document.getElementById("recentList"); | |
| threadDisplay.textContent = threadId; | |
| // ================= INIT ================= | |
| function generateId() { | |
| return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => { | |
| const r = Math.random() * 16 | 0; | |
| return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); | |
| }); | |
| } | |
| function saveMsgs() { | |
| localStorage.setItem("finai_msgs", JSON.stringify(messages)); | |
| updateRecent(); | |
| } | |
| function updateRecent() { | |
| let chats = JSON.parse(localStorage.getItem("finai_chats") || "[]"); | |
| if (messages.length > 0) { | |
| const first = messages.find(m => m.role === "user"); | |
| const title = first ? first.content.slice(0, 32) + (first.content.length > 32 ? "..." : "") : "New Chat"; | |
| const idx = chats.findIndex(c => c.id === threadId); | |
| const entry = { id: threadId, title, time: Date.now() }; | |
| if (idx >= 0) chats[idx] = entry; else chats.unshift(entry); | |
| chats = chats.slice(0, 15); | |
| localStorage.setItem("finai_chats", JSON.stringify(chats)); | |
| } | |
| recentList.innerHTML = ""; | |
| chats.forEach(c => { | |
| const el = document.createElement("div"); | |
| el.className = `recent-item${c.id === threadId ? " active" : ""}`; | |
| const d = new Date(c.time); | |
| const ts = d.toLocaleDateString() === new Date().toLocaleDateString() | |
| ? d.toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"}) | |
| : d.toLocaleDateString([], {month:"short", day:"numeric"}); | |
| el.innerHTML = `<div class="recent-dot"></div><div><div class="recent-title">${esc(c.title)}</div><div class="recent-time">${ts}</div></div>`; | |
| el.addEventListener("click", () => loadChat(c.id)); | |
| recentList.appendChild(el); | |
| }); | |
| } | |
| function loadChat(id) { | |
| const data = localStorage.getItem("finai_msgs_" + id); | |
| if (!data) { toast("Chat not found", "err"); return; } | |
| threadId = id; | |
| messages = JSON.parse(data); | |
| localStorage.setItem("finai_tid", threadId); | |
| threadDisplay.textContent = threadId; | |
| renderMsgs(); | |
| updateRecent(); | |
| sidebar.classList.remove("open"); | |
| sidebarOverlay.classList.remove("show"); | |
| } | |
| function renderMsgs() { | |
| messagesEl.innerHTML = ""; | |
| if (messages.length === 0) { | |
| messagesEl.appendChild(emptyState); | |
| chatHeader.style.display = "none"; | |
| return; | |
| } | |
| chatHeader.style.display = "flex"; | |
| messages.forEach(m => appendMsg(m.role, m.content, m.meta, m.perf, false)); | |
| scrollBottom(); | |
| } | |
| // ================= HELPERS ================= | |
| function toast(msg, type="info") { | |
| const el = document.createElement("div"); | |
| el.className = `toast ${type}`; | |
| const icon = type === "ok" ? "✓" : type === "err" ? "✕" : "•"; | |
| el.innerHTML = `<span style="font-weight:700;font-size:14px;">${icon}</span> ${msg}`; | |
| toasts.appendChild(el); | |
| setTimeout(() => { el.style.opacity="0"; el.style.transform="translateX(20px)"; setTimeout(()=>el.remove(),300); }, 4000); | |
| } | |
| function setStatus(text, active=false) { | |
| statusText.textContent = text; | |
| statusDot.classList.toggle("active", active); | |
| } | |
| function scrollBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; } | |
| function esc(t) { const d=document.createElement("div"); d.textContent=t; return d.innerHTML; } | |
| function fmtMd(t) { | |
| if (!t) return ""; | |
| return t | |
| .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>") | |
| .replace(/\*(.+?)\*/g, "<em>$1</em>") | |
| .replace(/`([^`]+)`/g, "<code>$1</code>") | |
| .replace(/\n/g, "<p></p>"); | |
| } | |
| // ================= MESSAGES ================= | |
| function appendMsg(role, content, meta=null, perf=null, animate=true) { | |
| if (messages.length === 0 && emptyState.parentNode) { | |
| emptyState.remove(); | |
| chatHeader.style.display = "flex"; | |
| } | |
| const msg = document.createElement("div"); | |
| msg.className = `msg${animate?" fade-in":""}`; | |
| const av = document.createElement("div"); | |
| av.className = `msg-avatar ${role}`; | |
| av.textContent = role === "user" ? "👤" : "🤖"; | |
| const body = document.createElement("div"); | |
| body.className = "msg-body"; | |
| const head = document.createElement("div"); | |
| head.className = "msg-head"; | |
| const tm = new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"}); | |
| head.innerHTML = `<span class="msg-name">${role==="user"?"You":"FinAI"}</span><span class="msg-time">${tm}</span>`; | |
| body.appendChild(head); | |
| const bubble = document.createElement("div"); | |
| bubble.className = "msg-text"; | |
| bubble.innerHTML = fmtMd(content); | |
| body.appendChild(bubble); | |
| if (perf && role === "assistant") { | |
| const p = document.createElement("div"); | |
| p.className = "perf"; | |
| p.innerHTML = ` | |
| <div class="perf-item"><span class="perf-dot"></span>${perf.tokens} tokens</div> | |
| <div class="perf-item">${perf.tps.toFixed(1)} tok/s</div> | |
| <div class="perf-item">${perf.latency.toFixed(2)}s</div> | |
| `; | |
| body.appendChild(p); | |
| } | |
| if (meta && role === "assistant") { | |
| body.appendChild(makeRag(meta)); | |
| } | |
| msg.appendChild(av); | |
| msg.appendChild(body); | |
| messagesEl.appendChild(msg); | |
| scrollBottom(); | |
| return msg; | |
| } | |
| function makeRag(meta) { | |
| const card = document.createElement("div"); | |
| card.className = "rag"; | |
| const used = meta.used_rag; | |
| const sources = meta.sources || []; | |
| const toggle = document.createElement("div"); | |
| toggle.className = "rag-toggle"; | |
| const bCls = used ? "rag-active" : "rag-direct"; | |
| const bTxt = used ? `📄 RAG · ${sources.length} source${sources.length!==1?"s":""}` : "🧠 Direct"; | |
| toggle.innerHTML = `<span class="rag-badge ${bCls}">${bTxt}</span><span class="rag-arrow">▼</span>`; | |
| const body = document.createElement("div"); | |
| body.className = "rag-body"; | |
| if (used && sources.length) { | |
| sources.forEach((s,i) => { | |
| const row = document.createElement("div"); | |
| row.className = "source"; | |
| row.style.animationDelay = `${i*0.06}s`; | |
| row.innerHTML = ` | |
| <div class="source-icon">📄</div> | |
| <div class="source-info"> | |
| <div class="source-name">${esc(s.document||"Unknown")}</div> | |
| <div class="source-meta">Page ${s.page||"?"}${s.chunk_index!==undefined?` · Chunk ${s.chunk_index}`:""}</div> | |
| </div> | |
| <div class="source-score">${(s.score||0.95).toFixed(3)}</div> | |
| `; | |
| body.appendChild(row); | |
| }); | |
| } else if (!used) { | |
| const info = document.createElement("div"); | |
| info.style.cssText = "font-size:11px;color:var(--text-dim);padding:4px 0;"; | |
| info.textContent = "Generated from fine-tuned Qwen model knowledge without document retrieval."; | |
| body.appendChild(info); | |
| } | |
| const foot = document.createElement("div"); | |
| foot.className = "rag-footer"; | |
| foot.innerHTML = `<span>Thread: ${meta.thread_id||threadId}</span><span>${new Date().toLocaleTimeString()}</span>`; | |
| body.appendChild(foot); | |
| toggle.addEventListener("click", () => { | |
| const open = body.classList.contains("open"); | |
| body.classList.toggle("open"); | |
| toggle.classList.toggle("open"); | |
| toggle.querySelector(".rag-arrow").textContent = open ? "▼" : "▲"; | |
| }); | |
| card.appendChild(toggle); | |
| card.appendChild(body); | |
| return card; | |
| } | |
| // ================= CHAT ================= | |
| async function sendMsg() { | |
| const text = inputField.value.trim(); | |
| if (!text || isStreaming) return; | |
| inputField.value = ""; | |
| inputField.style.height = "auto"; | |
| isStreaming = true; | |
| sendBtn.disabled = true; | |
| streamStart = performance.now(); | |
| tokCount = 0; | |
| setStatus("Generating...", true); | |
| messages.push({role:"user", content:text}); | |
| saveMsgs(); | |
| appendMsg("user", text); | |
| const asst = {role:"assistant", content:"", meta:null, perf:null}; | |
| messages.push(asst); | |
| const msgDiv = appendMsg("assistant", "", null, null); | |
| const bubble = msgDiv.querySelector(".msg-text"); | |
| bubble.innerHTML = `<div class="typing"><span></span><span></span><span></span></div>`; | |
| try { | |
| const res = await fetch(`${API_BASE}/chat/stream`, { | |
| method: "POST", | |
| headers: {"Content-Type":"application/json"}, | |
| body: JSON.stringify({message:text, thread_id:threadId}) | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buf = "", full = "", meta = null; | |
| 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.trim()) continue; | |
| const clean = line.replace(/^data: /,"").trim(); | |
| if (!clean) continue; | |
| try { | |
| const ev = JSON.parse(clean); | |
| if (ev.type === "token") { | |
| if (full === "") bubble.innerHTML = ""; | |
| full += ev.content; | |
| tokCount++; | |
| const elapsed = (performance.now()-streamStart)/1000; | |
| const tps = elapsed>0 ? tokCount/elapsed : 0; | |
| setStatus(`${tokCount} tokens · ${tps.toFixed(1)} tok/s`, true); | |
| bubble.innerHTML = fmtMd(full) + '<span class="stream-cursor"></span>'; | |
| scrollBottom(); | |
| } else if (ev.type === "metadata") { | |
| meta = ev; | |
| } else if (ev.type === "done") { | |
| break; | |
| } else if (ev.type === "error") { | |
| throw new Error(ev.message); | |
| } | |
| } catch(e) {} | |
| } | |
| } | |
| const total = (performance.now()-streamStart)/1000; | |
| const tps = total>0 ? tokCount/total : 0; | |
| bubble.innerHTML = fmtMd(full); | |
| asst.content = full; | |
| asst.meta = meta; | |
| asst.perf = {tokens:tokCount, tps, latency:total}; | |
| saveMsgs(); | |
| localStorage.setItem("finai_msgs_"+threadId, JSON.stringify(messages)); | |
| if (meta) { | |
| const rag = makeRag(meta); | |
| const body = msgDiv.querySelector(".msg-body"); | |
| const perfEl = body.querySelector(".perf"); | |
| body.insertBefore(rag, perfEl?.nextSibling || null); | |
| } | |
| setStatus("Ready", false); | |
| } catch(err) { | |
| bubble.innerHTML = `<span style="color:#ef4444">Error: ${esc(err.message)}</span>`; | |
| asst.content = `Error: ${err.message}`; | |
| saveMsgs(); | |
| toast(err.message, "err"); | |
| setStatus("Error", false); | |
| } finally { | |
| isStreaming = false; | |
| sendBtn.disabled = false; | |
| inputField.focus(); | |
| } | |
| } | |
| // ================= UPLOAD ================= | |
| 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"); | |
| if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener("change", () => { if (fileInput.files.length) handleFile(fileInput.files[0]); }); | |
| function handleFile(file) { | |
| if (file.type !== "application/pdf" && !file.name.endsWith(".pdf")) { | |
| toast("Only PDF files are supported", "err"); | |
| return; | |
| } | |
| currentFile = file; | |
| uzText.textContent = file.name; | |
| uzHint.textContent = `${(file.size/1024).toFixed(1)} KB · Click Upload to index`; | |
| uploadZone.style.borderColor = "var(--border-hover)"; | |
| uploadDone.classList.remove("show"); | |
| } | |
| uploadBtn.addEventListener("click", async () => { | |
| if (!currentFile) { toast("Select a PDF file first", "info"); return; } | |
| uploadProgress.classList.add("show"); | |
| uploadDone.classList.remove("show"); | |
| uploadBtn.disabled = true; | |
| const steps = [ | |
| {text:"Uploading PDF...", pct:"15%"}, | |
| {text:"Extracting text...", pct:"30%"}, | |
| {text:"Splitting into chunks...", pct:"50%"}, | |
| {text:"Creating embeddings...", pct:"70%"}, | |
| {text:"Storing in vector DB...", pct:"90%"}, | |
| {text:"Finalizing...", pct:"98%"} | |
| ]; | |
| let idx = 0; | |
| const interval = setInterval(() => { | |
| if (idx < steps.length) { | |
| progressStep.innerHTML = `<div class="spinner"></div> ${steps[idx].text}`; | |
| progressPct.textContent = steps[idx].pct; | |
| progressFill.style.width = steps[idx].pct; | |
| idx++; | |
| } | |
| }, 500); | |
| const formData = new FormData(); | |
| formData.append("file", currentFile); | |
| try { | |
| const res = await fetch(`${API_BASE}/upload`, {method:"POST", body:formData}); | |
| const data = await res.json(); | |
| clearInterval(interval); | |
| if (res.ok) { | |
| progressFill.style.width = "100%"; | |
| progressStep.innerHTML = `<div class="spinner"></div> Done!`; | |
| progressPct.textContent = "100%"; | |
| setTimeout(() => { | |
| uploadProgress.classList.remove("show"); | |
| udText.textContent = `Uploaded: ${data.filename || currentFile.name}`; | |
| udMeta.textContent = `${data.chunks || "?"} chunks indexed · Ready for queries`; | |
| uploadDone.classList.add("show"); | |
| toast(`"${currentFile.name}" indexed successfully`, "ok"); | |
| currentFile = null; | |
| uzText.textContent = "Drop PDF or click to browse"; | |
| uzHint.textContent = "Financial reports, statements, tax docs"; | |
| uploadZone.style.borderColor = ""; | |
| fileInput.value = ""; | |
| uploadBtn.disabled = false; | |
| }, 600); | |
| } else { | |
| throw new Error(data.detail || data.message || "Upload failed"); | |
| } | |
| } catch(err) { | |
| clearInterval(interval); | |
| uploadProgress.classList.remove("show"); | |
| progressFill.style.width = "0%"; | |
| toast(err.message, "err"); | |
| uploadBtn.disabled = false; | |
| } | |
| }); | |
| // ================= RESET ================= | |
| resetBtn.addEventListener("click", async () => { | |
| if (!confirm("Delete all uploaded documents? This cannot be undone.")) return; | |
| try { | |
| const res = await fetch(`${API_BASE}/reset`, {method:"DELETE"}); | |
| if (res.ok) { toast("All documents deleted", "ok"); uploadDone.classList.remove("show"); } | |
| else throw new Error("Failed"); | |
| } catch(err) { toast(err.message, "err"); } | |
| }); | |
| // ================= NEW CHAT ================= | |
| newChatBtn.addEventListener("click", () => { | |
| if (messages.length > 0) { | |
| localStorage.setItem("finai_msgs_"+threadId, JSON.stringify(messages)); | |
| } | |
| messages = []; | |
| threadId = generateId(); | |
| localStorage.setItem("finai_tid", threadId); | |
| localStorage.removeItem("finai_msgs"); | |
| threadDisplay.textContent = threadId; | |
| messagesEl.innerHTML = ""; | |
| messagesEl.appendChild(emptyState); | |
| chatHeader.style.display = "none"; | |
| updateRecent(); | |
| toast("New chat started", "info"); | |
| }); | |
| // ================= COPY ================= | |
| copyThread.addEventListener("click", () => { | |
| navigator.clipboard.writeText(threadId).then(() => { | |
| copyThread.textContent = "Copied!"; | |
| setTimeout(() => copyThread.textContent = "Copy", 1200); | |
| }); | |
| }); | |
| // ================= INPUT ================= | |
| inputField.addEventListener("input", () => { | |
| inputField.style.height = "auto"; | |
| inputField.style.height = Math.min(inputField.scrollHeight, 160) + "px"; | |
| }); | |
| inputField.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMsg(); } | |
| }); | |
| sendBtn.addEventListener("click", sendMsg); | |
| // ================= CHIPS ================= | |
| document.getElementById("emptyChips").addEventListener("click", e => { | |
| const chip = e.target.closest(".chip"); | |
| if (chip) { | |
| inputField.value = chip.dataset.q; | |
| inputField.focus(); | |
| inputField.style.height = "auto"; | |
| inputField.style.height = Math.min(inputField.scrollHeight, 160) + "px"; | |
| } | |
| }); | |
| // ================= MOBILE ================= | |
| mobileToggle.addEventListener("click", () => { | |
| sidebar.classList.toggle("open"); | |
| sidebarOverlay.classList.toggle("show"); | |
| }); | |
| sidebarOverlay.addEventListener("click", () => { | |
| sidebar.classList.remove("open"); | |
| sidebarOverlay.classList.remove("show"); | |
| }); | |
| // ================= BOOT ================= | |
| updateRecent(); | |
| if (messages.length > 0) renderMsgs(); | |
| </script> | |
| </body> | |
| </html> |