Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Mintoak Website Assistant</title> | |
| <meta name="description" content="Ask anything about Mintoak Innovations β products, payments, merchant onboarding, UPI, merchant engagement, and more." /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| /* βββ RESET & TOKENS βββ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --brand: #87BD28; | |
| --brand-dim: #6fa020; | |
| --brand-glow: rgba(135,189,40,.18); | |
| --brand-soft: rgba(135,189,40,.10); | |
| --radius-lg: 18px; | |
| --radius-md: 12px; | |
| --radius-sm: 8px; | |
| --font: 'Inter', system-ui, sans-serif; | |
| --sidebar-w: 280px; | |
| } | |
| /* Dark theme overrides */ | |
| [data-theme="dark"] { | |
| --bg: #0d0f12; | |
| --surface: #13161c; | |
| --surface2: #1a1e27; | |
| --surface3: #212630; | |
| --border: rgba(255,255,255,.07); | |
| --border-brand: rgba(135,189,40,.30); | |
| --text: #e8eaf0; | |
| --text-muted: #7a8190; | |
| --text-dim: #4a5060; | |
| --user-bg: #87BD28; | |
| --user-text: #0d0f12; | |
| --shadow: 0 8px 32px rgba(0,0,0,.45); | |
| --bg-gradient-1: rgba(135,189,40,.07); | |
| --bg-gradient-2: rgba(135,189,40,.05); | |
| --ticker-bg: rgba(19, 22, 28, 0.7); | |
| --header-bg: rgba(13, 15, 18, 0.85); | |
| --input-bg: rgba(13, 15, 18, 0.90); | |
| --logo-bg: #ffffff; | |
| --logo-shadow: 0 0 20px rgba(135,189,40,.35); | |
| --brand-text: #87BD28; /* Highly visible bright green on dark backgrounds */ | |
| --sidebar-bg: rgba(13, 15, 18, 0.95); | |
| --danger: #ff6b6b; | |
| --danger-soft: rgba(255,107,107,.10); | |
| --success: #51cf66; | |
| --success-soft: rgba(81,207,102,.10); | |
| } | |
| /* Light theme overrides */ | |
| [data-theme="light"] { | |
| --bg: #f7f9fc; | |
| --surface: #ffffff; | |
| --surface2: #edf1f6; | |
| --surface3: #e2e7ee; | |
| --border: rgba(0,0,0,.08); | |
| --border-brand: rgba(135,189,40,.40); | |
| --text: #1d2129; | |
| --text-muted: #5b6271; | |
| --text-dim: #8c95a5; | |
| --user-bg: #87BD28; | |
| --user-text: #0d0f12; | |
| --shadow: 0 8px 32px rgba(0,0,0,.06); | |
| --bg-gradient-1: rgba(135,189,40,.06); | |
| --bg-gradient-2: rgba(135,189,40,.03); | |
| --ticker-bg: rgba(237, 241, 246, 0.7); | |
| --header-bg: rgba(247, 249, 252, 0.85); | |
| --input-bg: rgba(247, 249, 252, 0.90); | |
| --logo-bg: #ffffff; | |
| --logo-shadow: 0 4px 12px rgba(0,0,0,.05); | |
| --brand-text: #4e7013; /* High-contrast darker forest green to meet WCAG AA contrast rules on light backgrounds */ | |
| --sidebar-bg: rgba(247, 249, 252, 0.97); | |
| --danger: #e03131; | |
| --danger-soft: rgba(224,49,49,.08); | |
| --success: #2f9e44; | |
| --success-soft: rgba(47,158,68,.08); | |
| } | |
| html, body { | |
| height: 100%; width: 100%; | |
| font-family: var(--font); | |
| background: var(--bg); | |
| color: var(--text); | |
| overflow: hidden; | |
| transition: background 0.3s, color 0.3s; | |
| } | |
| /* βββ ANIMATED BG βββ */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; z-index: 0; | |
| background: | |
| radial-gradient(ellipse 60% 50% at 20% 10%, var(--bg-gradient-1) 0%, transparent 70%), | |
| radial-gradient(ellipse 50% 60% at 80% 90%, var(--bg-gradient-2) 0%, transparent 70%); | |
| pointer-events: none; | |
| } | |
| /* βββ LAYOUT SHELL βββ */ | |
| #shell { | |
| position: relative; z-index: 1; | |
| display: flex; | |
| height: 100vh; | |
| width: 100%; | |
| } | |
| /* βββ SIDEBAR βββ */ | |
| #sidebar { | |
| width: var(--sidebar-w); | |
| min-width: var(--sidebar-w); | |
| height: 100vh; | |
| background: var(--sidebar-bg); | |
| backdrop-filter: blur(24px); | |
| -webkit-backdrop-filter: blur(24px); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s cubic-bezier(.4,0,.2,1), opacity 0.3s; | |
| overflow: hidden; | |
| flex-shrink: 0; | |
| } | |
| #sidebar.collapsed { | |
| transform: translateX(calc(-1 * var(--sidebar-w))); | |
| width: 0; | |
| min-width: 0; | |
| border-right: none; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .sidebar-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex-shrink: 0; | |
| } | |
| .sidebar-header h2 { | |
| font-size: 13px; | |
| font-weight: 700; | |
| color: var(--text); | |
| letter-spacing: -.2px; | |
| flex: 1; | |
| } | |
| .sidebar-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .sidebar-list::-webkit-scrollbar { width: 3px; } | |
| .sidebar-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| .sidebar-entry { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 12px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: all .15s; | |
| margin-bottom: 2px; | |
| position: relative; | |
| } | |
| .sidebar-entry:hover { background: var(--surface2); } | |
| .sidebar-entry.active { | |
| background: var(--brand-soft); | |
| border-left: 3px solid var(--brand); | |
| padding-left: 9px; | |
| } | |
| .sidebar-entry-text { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .sidebar-entry-title { | |
| font-size: 12.5px; | |
| font-weight: 500; | |
| color: var(--text); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .sidebar-entry-date { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| margin-top: 2px; | |
| } | |
| .sidebar-entry .delete-btn { | |
| opacity: 0; | |
| width: 24px; height: 24px; | |
| border: none; | |
| background: var(--danger-soft); | |
| color: var(--danger); | |
| border-radius: 6px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| transition: opacity .15s, background .15s; | |
| } | |
| .sidebar-entry:hover .delete-btn { opacity: 1; } | |
| .sidebar-entry .delete-btn:hover { background: var(--danger); color: #fff; } | |
| .sidebar-footer { | |
| padding: 12px; | |
| border-top: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .clear-all-btn { | |
| width: 100%; | |
| padding: 8px 0; | |
| font-size: 11.5px; | |
| font-weight: 600; | |
| color: var(--danger); | |
| background: var(--danger-soft); | |
| border: 1px solid transparent; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: all .15s; | |
| } | |
| .clear-all-btn:hover { background: var(--danger); color: #fff; } | |
| .sidebar-empty { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| color: var(--text-dim); | |
| font-size: 12px; | |
| } | |
| .sidebar-empty svg { opacity: .4; } | |
| /* βββ SIDEBAR OVERLAY (mobile) βββ */ | |
| #sidebar-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,.5); | |
| z-index: 99; | |
| } | |
| @media (max-width: 768px) { | |
| #sidebar { | |
| position: fixed; | |
| left: 0; top: 0; | |
| z-index: 100; | |
| box-shadow: 4px 0 24px rgba(0,0,0,.3); | |
| } | |
| #sidebar.collapsed { | |
| transform: translateX(calc(-1 * var(--sidebar-w) - 20px)); | |
| } | |
| #sidebar-overlay.visible { display: block; } | |
| } | |
| /* βββ MAIN CHAT AREA βββ */ | |
| #app { | |
| position: relative; | |
| display: flex; flex-direction: column; | |
| height: 100vh; | |
| flex: 1; | |
| min-width: 0; | |
| max-width: 100%; | |
| } | |
| /* βββ HEADER βββ */ | |
| header { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 14px 20px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--header-bg); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| flex-shrink: 0; | |
| transition: background 0.3s, border 0.3s; | |
| } | |
| .header-btn { | |
| display: flex; align-items: center; justify-content: center; | |
| gap: 6px; | |
| font-size: 11.5px; font-weight: 600; | |
| color: var(--text-muted); | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| padding: 5px 12px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| outline: none; | |
| white-space: nowrap; | |
| } | |
| .header-btn:hover { | |
| border-color: var(--border-brand) ; | |
| color: var(--brand) ; | |
| background: var(--brand-soft) ; | |
| } | |
| .header-btn.icon-only { | |
| width: 32px; height: 32px; | |
| padding: 0; | |
| border-radius: 50%; | |
| } | |
| .logo-wrap { | |
| width: 36px; height: 36px; | |
| background: var(--logo-bg); | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; | |
| padding: 5px; | |
| box-shadow: var(--logo-shadow); | |
| transition: background 0.3s, box-shadow 0.3s; | |
| } | |
| .logo-wrap img { width: 100%; height: 100%; object-fit: contain; } | |
| .header-title { | |
| display: flex; flex-direction: column; | |
| } | |
| .header-title h1 { font-size: 0; line-height: 0; } | |
| .header-title h1 img { height: 22px; vertical-align: middle; } | |
| .status-pill { | |
| display: flex; align-items: center; gap: 6px; | |
| font-size: 11px; font-weight: 500; color: var(--brand-text); | |
| background: var(--brand-soft); | |
| border: 1px solid var(--border-brand); | |
| padding: 4px 10px; border-radius: 20px; | |
| transition: all 0.3s; | |
| } | |
| .status-dot { | |
| width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--brand); | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%,100% { opacity:1; transform:scale(1); } | |
| 50% { opacity:.5; transform:scale(1.3); } | |
| } | |
| /* βββ MESSAGES AREA βββ */ | |
| #messages { | |
| flex: 1; overflow-y: auto; | |
| padding: 24px 20px; | |
| display: flex; flex-direction: column; | |
| align-items: center; | |
| gap: 20px; | |
| scroll-behavior: smooth; | |
| position: relative; | |
| } | |
| #messages::-webkit-scrollbar { width: 4px; } | |
| #messages::-webkit-scrollbar-track { background: transparent; } | |
| #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } | |
| /* βββ WELCOME STATE βββ */ | |
| #welcome { | |
| display: flex; flex-direction: column; align-items: center; | |
| justify-content: center; text-align: center; | |
| margin: auto; padding: 40px 20px; | |
| gap: 16px; | |
| width: 100%; | |
| max-width: 720px; | |
| } | |
| #welcome .big-logo { | |
| width: 80px; height: 80px; | |
| background: var(--logo-bg); | |
| border-radius: 20px; | |
| display: flex; align-items: center; justify-content: center; | |
| padding: 12px; | |
| box-shadow: var(--logo-shadow); | |
| animation: float 3s ease-in-out infinite; | |
| transition: background 0.3s, box-shadow 0.3s; | |
| } | |
| @keyframes float { | |
| 0%,100% { transform:translateY(0); } | |
| 50% { transform:translateY(-8px); } | |
| } | |
| #welcome .big-logo img { width: 100%; height: 100%; object-fit: contain; } | |
| #welcome h2 { font-size: 22px; font-weight: 700; color: var(--text); transition: color 0.3s; animation: fadeInUp .5s ease .1s both; } | |
| #welcome p { font-size: 14px; color: var(--text-muted); max-width: 360px; line-height: 1.6; transition: color 0.3s; animation: fadeInUp .5s ease .2s both; } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(12px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .starter-chips { | |
| display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .starter-chip { | |
| font-size: 13px; color: var(--brand-text); | |
| background: var(--brand-soft); | |
| border: 1px solid var(--border-brand); | |
| padding: 8px 14px; border-radius: 20px; | |
| cursor: pointer; transition: all .2s; | |
| animation: fadeInUp .4s ease both; | |
| } | |
| .starter-chip:nth-child(1) { animation-delay: .3s; } | |
| .starter-chip:nth-child(2) { animation-delay: .4s; } | |
| .starter-chip:nth-child(3) { animation-delay: .5s; } | |
| .starter-chip:nth-child(4) { animation-delay: .6s; } | |
| .starter-chip:hover { | |
| background: var(--brand); color: #0d0f12; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 16px rgba(135,189,40,.35); | |
| } | |
| /* βββ MESSAGE ROW βββ */ | |
| .msg-row { | |
| display: flex; align-items: flex-start; gap: 10px; | |
| animation: slideUp .3s ease both; | |
| width: 100%; | |
| max-width: 720px; | |
| } | |
| @keyframes slideUp { | |
| from { opacity:0; transform:translateY(12px); } | |
| to { opacity:1; transform:translateY(0); } | |
| } | |
| .msg-row.user { flex-direction: row-reverse; } | |
| /* βββ AVATAR βββ */ | |
| .avatar { | |
| width: 24px; height: 24px; border-radius: 8px; | |
| flex-shrink: 0; display: flex; align-items: center; justify-content: center; | |
| font-size: 12px; font-weight: 700; | |
| margin-top: 2px; | |
| } | |
| .avatar.bot { | |
| background: var(--logo-bg); | |
| padding: 3px; | |
| box-shadow: none; | |
| overflow: hidden; | |
| transition: background 0.3s; | |
| } | |
| .avatar.bot img { width: 100%; height: 100%; object-fit: contain; } | |
| .avatar.user-av { | |
| display: none; /* GPT-style: hide user avatar */ | |
| } | |
| /* βββ BUBBLE βββ */ | |
| .bubble-wrap { display: flex; flex-direction: column; max-width: 100%; gap: 6px; flex: 1; min-width: 0; } | |
| .msg-row.user .bubble-wrap { align-items: flex-end; } | |
| .bubble { | |
| font-size: 14px; line-height: 1.65; | |
| word-break: break-word; | |
| position: relative; | |
| } | |
| .bubble.bot { | |
| background: transparent; | |
| border: none; | |
| color: var(--text); | |
| padding: 2px 0; | |
| transition: color 0.3s; | |
| } | |
| .bubble.user-b { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| font-weight: 500; | |
| padding: 10px 16px; | |
| border-radius: 20px; | |
| display: inline-block; | |
| } | |
| /* markdown inside bot bubble */ | |
| .bubble.bot strong { color: var(--brand-text); } | |
| .bubble.bot a { | |
| color: var(--brand-text); | |
| text-decoration: underline; | |
| text-underline-offset: 3px; | |
| font-weight: 500; | |
| transition: color .2s; | |
| } | |
| .bubble.bot a:hover { | |
| color: var(--brand); | |
| } | |
| .bubble.bot code { | |
| background: rgba(135,189,40,.12); color: var(--brand-text); | |
| padding: 1px 5px; border-radius: 4px; font-size: 12px; | |
| } | |
| .bubble.bot ul, .bubble.bot ol { padding-left: 18px; margin-top: 4px; } | |
| .bubble.bot li { margin-bottom: 3px; } | |
| /* βββ HOVER ACTIONS (copy + feedback) βββ */ | |
| .msg-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity .2s; | |
| margin-top: 2px; | |
| } | |
| .msg-row:hover .msg-actions, | |
| .msg-actions.visible { opacity: 1; } | |
| .action-btn { | |
| display: flex; align-items: center; justify-content: center; | |
| width: 28px; height: 28px; | |
| border: 1px solid var(--border); | |
| background: var(--surface); | |
| color: var(--text-muted); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all .15s; | |
| position: relative; | |
| } | |
| .action-btn:hover { border-color: var(--border-brand); color: var(--brand); background: var(--brand-soft); } | |
| .action-btn.active-up { border-color: var(--brand); color: var(--brand); background: var(--success-soft); } | |
| .action-btn.active-down { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); } | |
| .action-btn svg { width: 14px; height: 14px; } | |
| .copy-toast { | |
| position: absolute; | |
| top: -28px; left: 50%; | |
| transform: translateX(-50%); | |
| background: var(--surface3); | |
| color: var(--text); | |
| font-size: 10px; | |
| font-weight: 600; | |
| padding: 3px 8px; | |
| border-radius: 6px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| transition: opacity .2s; | |
| pointer-events: none; | |
| } | |
| .copy-toast.show { opacity: 1; } | |
| /* βββ SOURCES βββ */ | |
| .sources-container { | |
| margin-top: 14px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| width: 100%; | |
| align-items: flex-start; | |
| } | |
| .sources-label { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-dim); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .sources-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .source-card { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--brand-text); | |
| background: var(--brand-soft); | |
| border: 1px solid var(--border-brand); | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| text-decoration: none; | |
| transition: all .2s ease; | |
| } | |
| .source-card:hover { | |
| background: var(--brand); | |
| color: #0d0f12; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(135,189,40,.2); | |
| border-color: var(--brand); | |
| } | |
| .source-card svg { width: 12px; height: 12px; flex-shrink: 0; stroke: currentColor; } | |
| /* βββ SUGGESTION CHIPS βββ */ | |
| .suggestions-row { | |
| display: flex; flex-wrap: wrap; gap: 6px; | |
| margin-top: 2px; | |
| } | |
| .suggestion-chip { | |
| font-size: 12px; color: var(--text-muted); | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| padding: 5px 12px; border-radius: 20px; | |
| cursor: pointer; transition: all .2s; | |
| } | |
| .suggestion-chip:hover { border-color: var(--border-brand); color: var(--brand); background: var(--brand-soft); } | |
| .msg-time { font-size: 10px; color: var(--text-dim); margin-top: 4px; } | |
| /* βββ TYPING INDICATOR βββ */ | |
| .typing-row { | |
| display: flex; align-items: flex-end; gap: 10px; | |
| animation: slideUp .3s ease both; | |
| width: 100%; | |
| max-width: 720px; | |
| } | |
| .typing-bubble { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| padding: 12px 16px; | |
| border-radius: var(--radius-lg); | |
| border-bottom-left-radius: 4px; | |
| display: flex; gap: 4px; align-items: center; | |
| transition: background 0.3s, border 0.3s; | |
| } | |
| .typing-bubble .dot { | |
| width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--text-muted); | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| } | |
| .typing-bubble .dot:nth-child(1) { animation-delay: -.32s; } | |
| .typing-bubble .dot:nth-child(2) { animation-delay: -.16s; } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { transform: scale(0); } | |
| 40% { transform: scale(1.0); } | |
| } | |
| .latency-badge { | |
| display: flex; align-items: center; gap: 4px; | |
| font-size: 9px; font-weight: 600; color: var(--text-dim); | |
| border: 1px solid var(--border); | |
| padding: 1px 5px; border-radius: 4px; | |
| } | |
| .latency-badge svg { width: 10px; height: 10px; } | |
| /* βββ SCROLL-TO-BOTTOM FAB βββ */ | |
| #scroll-fab { | |
| position: absolute; | |
| bottom: 16px; | |
| left: 50%; | |
| transform: translateX(-50%) scale(0); | |
| width: 36px; height: 36px; | |
| background: var(--brand); | |
| border: none; | |
| border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; | |
| box-shadow: 0 4px 16px rgba(135,189,40,.4); | |
| transition: transform .2s cubic-bezier(.4,0,.2,1), opacity .2s; | |
| opacity: 0; | |
| z-index: 10; | |
| } | |
| #scroll-fab.visible { | |
| transform: translateX(-50%) scale(1); | |
| opacity: 1; | |
| } | |
| #scroll-fab:hover { transform: translateX(-50%) scale(1.1); } | |
| #scroll-fab svg { width: 18px; height: 18px; } | |
| /* βββ INPUT BAR βββ */ | |
| #input-bar { | |
| padding: 14px 20px 18px; | |
| border-top: 1px solid var(--border); | |
| background: var(--input-bg); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| flex-shrink: 0; | |
| transition: background 0.3s, border 0.3s; | |
| } | |
| .input-wrap { | |
| display: flex; align-items: flex-end; gap: 10px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| padding: 10px 10px 10px 16px; | |
| transition: border-color .2s, box-shadow .2s, background 0.3s, border 0.3s; | |
| max-width: 720px; | |
| margin: 0 auto; | |
| } | |
| .input-wrap:focus-within { | |
| border-color: var(--border-brand); | |
| box-shadow: 0 0 0 3px rgba(135,189,40,.10); | |
| } | |
| #query { | |
| flex: 1; border: none; background: transparent; | |
| color: var(--text); font-family: var(--font); | |
| font-size: 14px; line-height: 1.5; | |
| resize: none; max-height: 120px; | |
| outline: none; | |
| transition: color 0.3s; | |
| } | |
| #query::placeholder { color: var(--text-dim); } | |
| #send-btn { | |
| width: 38px; height: 38px; | |
| background: var(--brand); | |
| border: none; border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; flex-shrink: 0; | |
| transition: all .2s; | |
| } | |
| #send-btn:hover { background: var(--brand-dim); transform: scale(1.06); } | |
| #send-btn:active { transform: scale(.96); } | |
| #send-btn:disabled { background: var(--surface); cursor: not-allowed; transform: none; } | |
| #send-btn svg { width: 18px; height: 18px; } | |
| .input-footer { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-top: 8px; | |
| padding: 0 4px; | |
| max-width: 720px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .input-hint { | |
| font-size: 11px; color: var(--text-dim); | |
| transition: color 0.3s; | |
| } | |
| .input-hint kbd { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| padding: 1px 5px; border-radius: 4px; | |
| font-family: var(--font); font-size: 10px; | |
| transition: background 0.3s, border 0.3s; | |
| } | |
| .char-counter { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| transition: color .2s; | |
| } | |
| .char-counter.warn { color: #f59f00; } | |
| .char-counter.max { color: var(--danger); font-weight: 600; } | |
| /* βββ LEAD CAPTURE FORM CARD βββ */ | |
| .lead-form-card { | |
| background: var(--surface2); | |
| border: 1px solid var(--border-brand); | |
| border-radius: var(--radius-md); | |
| padding: 16px; | |
| margin-top: 14px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| width: 100%; | |
| max-width: 420px; | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.15); | |
| animation: fadeInUp 0.4s ease both; | |
| } | |
| .lead-form-card h3 { | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: var(--brand-text); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .lead-form-card p { | |
| font-size: 12.5px; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| } | |
| .lead-form-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .lead-input { | |
| width: 100%; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| font-family: var(--font); | |
| font-size: 13px; | |
| padding: 8px 12px; | |
| border-radius: var(--radius-sm); | |
| outline: none; | |
| transition: all 0.2s; | |
| } | |
| .lead-input:focus { | |
| border-color: var(--brand); | |
| box-shadow: 0 0 0 2px var(--brand-soft); | |
| } | |
| .lead-submit-btn { | |
| background: var(--brand); | |
| color: #0d0f12; | |
| border: none; | |
| font-family: var(--font); | |
| font-size: 13px; | |
| font-weight: 600; | |
| padding: 9px 16px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| transition: all 0.2s; | |
| } | |
| .lead-submit-btn:hover { | |
| background: var(--brand-dim); | |
| } | |
| .lead-submit-btn:disabled { | |
| background: var(--border); | |
| color: var(--text-dim); | |
| cursor: not-allowed; | |
| } | |
| .lead-success-msg { | |
| font-size: 13px; | |
| color: var(--brand-text); | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| animation: fadeInUp 0.3s ease both; | |
| } | |
| /* βββ RESPONSIVE βββ */ | |
| @media (max-width: 600px) { | |
| header { padding: 12px 14px; gap: 8px; } | |
| #messages { padding: 16px 12px; } | |
| #input-bar { padding: 10px 12px 14px; } | |
| .bubble-wrap { max-width: 88%; } | |
| .starter-chips { gap: 6px; } | |
| .starter-chip { font-size: 12px; padding: 6px 12px; } | |
| } | |
| </style> | |
| </head> | |
| <body data-theme="dark"> | |
| <!-- SIDEBAR OVERLAY (mobile) --> | |
| <div id="sidebar-overlay" onclick="toggleSidebar()"></div> | |
| <div id="shell"> | |
| <!-- SIDEBAR --> | |
| <aside id="sidebar" class="collapsed"> | |
| <div class="sidebar-header"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand);flex-shrink:0;"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> | |
| <h2>Chat History</h2> | |
| <button class="header-btn icon-only" onclick="toggleSidebar()" title="Close Sidebar" style="width:28px;height:28px;"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> | |
| </button> | |
| </div> | |
| <div class="sidebar-list" id="sidebar-list"> | |
| <!-- Populated by JS --> | |
| </div> | |
| <div class="sidebar-footer" id="sidebar-footer" style="display:none;"> | |
| <button class="clear-all-btn" onclick="clearAllHistory()"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg> | |
| Clear All History | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- MAIN CHAT AREA --> | |
| <div id="app"> | |
| <!-- HEADER --> | |
| <header> | |
| <!-- Sidebar toggle --> | |
| <button class="header-btn icon-only" onclick="toggleSidebar()" title="Chat History" id="sidebar-toggle-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> | |
| </button> | |
| <div class="header-title"> | |
| <h1><img src="/static/mintoak_logo.svg" alt="Mintoak" /></h1> | |
| </div> | |
| <!-- NEW CHAT --> | |
| <button class="header-btn" onclick="startNewChat()" title="Start New Chat (Ctrl+Shift+N)" style="margin-left:auto;"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> | |
| New Chat | |
| </button> | |
| <!-- THEME TOGGLE --> | |
| <button class="header-btn icon-only" onclick="toggleTheme()" title="Toggle Light/Dark Mode" id="theme-toggle-btn"> | |
| <svg id="theme-icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.22" x2="5.64" y2="17.78"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> | |
| <svg id="theme-icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> | |
| </button> | |
| <!-- STATUS --> | |
| <div class="status-pill"> | |
| <div class="status-dot"></div> | |
| Online | |
| </div> | |
| </header> | |
| <!-- TICKER BANNER --> | |
| <div id="ticker" style="border-bottom:1px solid var(--border);padding:7px 20px;font-size:11.5px;color:var(--text-muted);display:flex;align-items:center;justify-content:center;gap:8px;flex-shrink:0;background:var(--ticker-bg);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);transition:background .3s,border .3s;"> | |
| <span style="color:var(--brand);font-weight:700;text-transform:uppercase;letter-spacing:.6px;display:inline-flex;align-items:center;gap:4px;flex-shrink:0;"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Spotlight: | |
| </span> | |
| <span id="ticker-text" style="transition:opacity .4s ease;opacity:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;">Powers merchant platforms for leading banks like HDFC, SBI & Axis Bank.</span> | |
| </div> | |
| <!-- MESSAGES --> | |
| <div id="messages"> | |
| <!-- WELCOME --> | |
| <div id="welcome"> | |
| <div class="big-logo"> | |
| <img src="/static/Logomark.svg" alt="Mintoak" /> | |
| </div> | |
| <h2>Hello! I'm Mintoak Website Assistant π</h2> | |
| <p>Ask me anything about Mintoak Innovations β products, payments, merchant onboarding, UPI, merchant engagement, and more!</p> | |
| <div class="starter-chips"> | |
| <div class="starter-chip" onclick="sendStarter(this)">What products does Mintoak offer?</div> | |
| <div class="starter-chip" onclick="sendStarter(this)">How does UPI help merchants?</div> | |
| <div class="starter-chip" onclick="sendStarter(this)">What is merchant onboarding?</div> | |
| <div class="starter-chip" onclick="sendStarter(this)">Tell me about Mintoak's merchant engagement platform</div> | |
| </div> | |
| </div> | |
| <!-- SCROLL-TO-BOTTOM FAB --> | |
| <button id="scroll-fab" onclick="scrollBottom()" title="Scroll to bottom"> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M19 12l-7 7-7-7" stroke="#0d0f12" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| </button> | |
| </div> | |
| <!-- INPUT BAR --> | |
| <div id="input-bar"> | |
| <div class="input-wrap"> | |
| <textarea id="query" rows="1" placeholder="Ask me anything about Mintoakβ¦" autocomplete="off" maxlength="500"></textarea> | |
| <button id="send-btn" onclick="sendMessage()" title="Send (Enter)"> | |
| <svg viewBox="0 0 24 24" fill="none"> | |
| <path d="M22 2L11 13" stroke="#0d0f12" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="#0d0f12" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="input-footer"> | |
| <div class="input-hint">Press <kbd>Enter</kbd> to send Β· <kbd>Shift+Enter</kbd> for new line</div> | |
| <div class="char-counter" id="char-counter">0 / 500</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ Theme toggle logic ββ | |
| function initTheme() { | |
| const savedTheme = localStorage.getItem('theme') || 'dark'; | |
| document.documentElement.setAttribute('data-theme', savedTheme); | |
| document.body.setAttribute('data-theme', savedTheme); | |
| updateThemeIcons(savedTheme); | |
| } | |
| function toggleTheme() { | |
| const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; | |
| const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; | |
| document.documentElement.setAttribute('data-theme', newTheme); | |
| document.body.setAttribute('data-theme', newTheme); | |
| localStorage.setItem('theme', newTheme); | |
| updateThemeIcons(newTheme); | |
| } | |
| function updateThemeIcons(theme) { | |
| const sunIcon = document.getElementById('theme-icon-sun'); | |
| const moonIcon = document.getElementById('theme-icon-moon'); | |
| if (theme === 'dark') { | |
| sunIcon.style.display = 'block'; | |
| moonIcon.style.display = 'none'; | |
| } else { | |
| sunIcon.style.display = 'none'; | |
| moonIcon.style.display = 'block'; | |
| } | |
| } | |
| initTheme(); | |
| const messagesEl = document.getElementById('messages'); | |
| const queryEl = document.getElementById('query'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const welcomeEl = document.getElementById('welcome'); | |
| const scrollFab = document.getElementById('scroll-fab'); | |
| const charCounter = document.getElementById('char-counter'); | |
| // Capture original welcome HTML to restore it on 'New Chat' | |
| const WELCOME_HTML = welcomeEl.outerHTML; | |
| let currentAbortController = null; | |
| let isGenerating = false; | |
| let activeQuery = ''; | |
| let hasStarted = false; | |
| let botRowEl = null; | |
| let bubbleWrapEl = null; | |
| let botBubbleEl = null; | |
| let activeSources = null; | |
| // Rate Limiting & Anti-Spam | |
| let messageTimestamps = []; | |
| let isRateBlocked = false; | |
| // History State | |
| let currentConvId = null; | |
| let conversations = []; | |
| const STORAGE_KEY = 'mintoak_chat_history'; | |
| const MAX_CONVERSATIONS = 150; | |
| const MINTOAK_FACTS = [ | |
| "Mintoak powers the merchant platforms for leading banks like HDFC Bank, SBI, and Axis Bank.", | |
| "Mintoak turns standard payment terminals into merchant engagement hubs with real-time analytics.", | |
| "Mintoak's Business360 enables merchants to analyze sales patterns and monitor store performance.", | |
| "Mintoak's digital onboarding platform reduces merchant registration times from days to minutes.", | |
| "Mintoak helps merchants design custom loyalty and engagement campaigns, driving repeat visits and customer lifetime value.", | |
| "Mintoak operates across international markets, including India, Africa, and the Middle East." | |
| ]; | |
| // Configure marked for proper line breaks and settings | |
| marked.setOptions({ | |
| breaks: true, | |
| gfm: true | |
| }); | |
| // Load chats from LocalStorage | |
| function loadConversations() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| conversations = raw ? JSON.parse(raw) : []; | |
| } catch { | |
| conversations = []; | |
| } | |
| } | |
| // Save chats to LocalStorage | |
| function saveConversations() { | |
| if (conversations.length > MAX_CONVERSATIONS) { | |
| conversations = conversations.slice(0, MAX_CONVERSATIONS); | |
| } | |
| try { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(conversations)); | |
| } catch (e) { | |
| // LocalStorage full, pop oldest | |
| while (conversations.length > 5) { | |
| conversations.pop(); | |
| try { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(conversations)); | |
| break; | |
| } catch {} | |
| } | |
| } | |
| } | |
| function getConversation(id) { | |
| return conversations.find(c => c.id === id); | |
| } | |
| function createConversation() { | |
| const conv = { | |
| id: 'conv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), | |
| title: 'New Chat', | |
| createdAt: new Date().toISOString(), | |
| messages: [] | |
| }; | |
| conversations.unshift(conv); | |
| saveConversations(); | |
| return conv; | |
| } | |
| function deleteConversation(id) { | |
| conversations = conversations.filter(c => c.id !== id); | |
| saveConversations(); | |
| if (currentConvId === id) { | |
| currentConvId = null; | |
| showWelcome(); | |
| } | |
| renderSidebar(); | |
| } | |
| function clearAllHistory() { | |
| if (!confirm('Delete all chat history? This cannot be undone.')) return; | |
| conversations = []; | |
| saveConversations(); | |
| currentConvId = null; | |
| showWelcome(); | |
| renderSidebar(); | |
| } | |
| function addMessageToConv(convId, msg) { | |
| const conv = getConversation(convId); | |
| if (!conv) return; | |
| conv.messages.push(msg); | |
| if (msg.role === 'user' && conv.messages.filter(m => m.role === 'user').length === 1) { | |
| conv.title = msg.content.slice(0, 40) + (msg.content.length > 40 ? '...' : ''); | |
| generateConvTitle(convId, msg.content); | |
| } | |
| saveConversations(); | |
| renderSidebar(); | |
| } | |
| async function generateConvTitle(convId, firstMsg) { | |
| try { | |
| const res = await fetch('/api/title', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: firstMsg }) | |
| }); | |
| const data = await res.json(); | |
| if (data.title) { | |
| const conv = getConversation(convId); | |
| if (conv) { | |
| conv.title = data.title; | |
| saveConversations(); | |
| renderSidebar(); | |
| } | |
| } | |
| } catch {} | |
| } | |
| function setFeedback(convId, msgIndex, feedback) { | |
| const conv = getConversation(convId); | |
| if (!conv || !conv.messages[msgIndex]) return; | |
| conv.messages[msgIndex].feedback = feedback; | |
| saveConversations(); | |
| } | |
| // ββ SIDEBAR RENDER βββββββββββββββββββββββββ | |
| const sidebar = document.getElementById('sidebar'); | |
| const sidebarList = document.getElementById('sidebar-list'); | |
| const sidebarFooter = document.getElementById('sidebar-footer'); | |
| const overlay = document.getElementById('sidebar-overlay'); | |
| function toggleSidebar() { | |
| sidebar.classList.toggle('collapsed'); | |
| if (!sidebar.classList.contains('collapsed')) { | |
| overlay.classList.add('visible'); | |
| } else { | |
| overlay.classList.remove('visible'); | |
| } | |
| } | |
| function renderSidebar() { | |
| if (!conversations.length) { | |
| sidebarList.innerHTML = ` | |
| <div class="sidebar-empty"> | |
| <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> | |
| <div>No conversations yet.<br/>Start chatting to see your history here.</div> | |
| </div>`; | |
| sidebarFooter.style.display = 'none'; | |
| return; | |
| } | |
| sidebarFooter.style.display = 'block'; | |
| const html = conversations.map(c => { | |
| const isActive = c.id === currentConvId; | |
| const date = new Date(c.createdAt); | |
| const dateStr = date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' Β· ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| return ` | |
| <div class="sidebar-entry ${isActive ? 'active' : ''}" onclick="loadConversation('${c.id}')" data-id="${c.id}"> | |
| <div class="sidebar-entry-text"> | |
| <div class="sidebar-entry-title">${escapeHtml(c.title)}</div> | |
| <div class="sidebar-entry-date">${dateStr}</div> | |
| </div> | |
| <button class="delete-btn" onclick="event.stopPropagation(); deleteConversation('${c.id}')" title="Delete"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg> | |
| </button> | |
| </div>`; | |
| }).join(''); | |
| sidebarList.innerHTML = html; | |
| } | |
| function escapeHtml(str) { | |
| const div = document.createElement('div'); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| // Reload conversation | |
| function loadConversation(convId) { | |
| if (isGenerating) stopGenerating(); | |
| currentConvId = convId; | |
| const conv = getConversation(convId); | |
| if (!conv) return; | |
| // Clear messages | |
| messagesEl.querySelectorAll('.msg-row, .typing-row, #welcome').forEach(el => el.remove()); | |
| // Re-render | |
| conv.messages.forEach((msg, idx) => { | |
| if (msg.role === 'user') { | |
| appendUserBubble(msg.content, msg.time); | |
| } else { | |
| appendBotBubble(msg.content, msg.sources, msg.suggestions, msg.time, msg.latency, idx, convId, msg.feedback, msg.leadCapture, msg.leadSubmitted, msg.leadName, msg.leadEmail); | |
| } | |
| }); | |
| renderSidebar(); | |
| scrollBottom(); | |
| if (window.innerWidth <= 768) toggleSidebar(); | |
| } | |
| function showWelcome() { | |
| messagesEl.querySelectorAll('.msg-row, .typing-row, #welcome').forEach(el => el.remove()); | |
| const w = document.createElement('div'); | |
| w.id = 'welcome'; | |
| w.innerHTML = WELCOME_HTML; | |
| messagesEl.insertBefore(w, scrollFab); | |
| } | |
| // Client-side quick keyword mapping for suggestions on stop | |
| function getClientSuggestions(query) { | |
| const q = query.toLowerCase(); | |
| const TOPIC_MAP = [ | |
| [["upi", "unified payment"], [ | |
| "What are the transaction fees for UPI payments?", | |
| "How does Mintoak support UPI for small merchants?", | |
| "Can UPI payments be tracked in real time?" | |
| ]], | |
| [["product", "offering", "catalog", "what do you offer"], [ | |
| "Tell me more about Mintoak's merchant engagement platform", | |
| "How does merchant onboarding work with Mintoak?", | |
| "What payment methods does Mintoak support?" | |
| ]], | |
| [["loyalty", "reward", "cashback", "points"], [ | |
| "How do customers earn loyalty points?", | |
| "Can merchants customize their loyalty offers?", | |
| "How does Mintoak's merchant engagement platform benefit banks?" | |
| ]], | |
| [["merchant", "onboard", "kyc", "kyb"], [ | |
| "What documents are needed for merchant onboarding?", | |
| "How long does merchant onboarding take with Mintoak?", | |
| "What is the difference between KYC and KYB?" | |
| ]], | |
| [["bank", "banking", "financial", "payment"], [ | |
| "How does Mintoak help banks acquire merchants?", | |
| "What analytics does Mintoak provide to banks?", | |
| "How does Mintoak's white-label solution work?" | |
| ]], | |
| [["credit card", "debit card", "card"], [ | |
| "What card payment types does Mintoak support?", | |
| "How do business credit cards help merchants?", | |
| "What are the benefits of card acceptance for SMEs?" | |
| ]], | |
| [["sme", "small business", "merchant account"], [ | |
| "How can SMEs benefit from digital payments?", | |
| "How does Mintoak help SMEs grow?", | |
| "What is a merchant account and why do I need one?" | |
| ]], | |
| [["africa", "african", "india", "indian"], [ | |
| "How does Mintoak operate across different markets?", | |
| "What is Mintoak's approach to emerging markets?", | |
| "How does digital merchant onboarding work in Africa?" | |
| ]] | |
| ]; | |
| for (const [keywords, suggestions] of TOPIC_MAP) { | |
| if (keywords.some(kw => q.includes(kw))) { | |
| return suggestions; | |
| } | |
| } | |
| return [ | |
| "What products does Mintoak offer?", | |
| "How can Mintoak help my business grow?", | |
| "How do I get in touch with the Mintoak team?" | |
| ]; | |
| } | |
| // Toggle button between Send and Stop modes | |
| function setButtonMode(mode) { | |
| if (mode === 'stop') { | |
| sendBtn.innerHTML = ` | |
| <svg viewBox="0 0 24 24" fill="none"> | |
| <rect x="5" y="5" width="14" height="14" rx="2" fill="#0d0f12" stroke="#0d0f12" stroke-width="2.2"/> | |
| </svg> | |
| `; | |
| sendBtn.title = "Stop Generating"; | |
| sendBtn.onclick = stopGenerating; | |
| sendBtn.disabled = false; | |
| } else { | |
| sendBtn.innerHTML = ` | |
| <svg viewBox="0 0 24 24" fill="none"> | |
| <path d="M22 2L11 13" stroke="#0d0f12" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="#0d0f12" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| `; | |
| sendBtn.title = "Send (Enter)"; | |
| sendBtn.onclick = sendMessage; | |
| sendBtn.disabled = false; | |
| } | |
| } | |
| // Abort active LLM response generation stream and append suggestion chips | |
| function stopGenerating() { | |
| if (currentAbortController) { | |
| currentAbortController.abort(); | |
| } | |
| if (isGenerating) { | |
| removeTyping(); | |
| if (!hasStarted) { | |
| hasStarted = true; | |
| botRowEl = document.createElement('div'); | |
| botRowEl.className = 'msg-row'; | |
| botRowEl.innerHTML = ` | |
| <div class="avatar bot"><img src="/static/Logomark.svg" alt="M" /></div> | |
| <div class="bubble-wrap"> | |
| <div class="bubble bot" style="font-style: italic; color: var(--text-muted);">Generation stopped.</div> | |
| </div> | |
| `; | |
| messagesEl.insertBefore(botRowEl, scrollFab); | |
| bubbleWrapEl = botRowEl.querySelector('.bubble-wrap'); | |
| botBubbleEl = botRowEl.querySelector('.bubble.bot'); | |
| } | |
| const suggestions = getClientSuggestions(activeQuery); | |
| if (suggestions && suggestions.length) { | |
| const chips = suggestions.map(s => | |
| `<div class="suggestion-chip" onclick="sendSuggestion(this)">${s}</div>` | |
| ).join(''); | |
| const suggestionsRow = document.createElement('div'); | |
| suggestionsRow.className = 'suggestions-row'; | |
| suggestionsRow.innerHTML = chips; | |
| bubbleWrapEl.appendChild(suggestionsRow); | |
| } | |
| const footerRow = document.createElement('div'); | |
| footerRow.style.display = 'flex'; | |
| footerRow.style.gap = '10px'; | |
| footerRow.style.alignItems = 'center'; | |
| footerRow.innerHTML = ` | |
| <div class="msg-time">${nowTime()}</div> | |
| <div style="font-size: 10px; color: var(--text-dim);">Β· Stopped by user</div> | |
| `; | |
| bubbleWrapEl.appendChild(footerRow); | |
| scrollBottom(); | |
| } | |
| isGenerating = false; | |
| setButtonMode('send'); | |
| queryEl.focus(); | |
| } | |
| function startNewChat() { | |
| stopGenerating(); | |
| currentConvId = null; | |
| showWelcome(); | |
| queryEl.value = ''; | |
| queryEl.style.height = 'auto'; | |
| updateCharCounter(); | |
| renderSidebar(); | |
| } | |
| // Resizing and character counter | |
| queryEl.addEventListener('input', () => { | |
| queryEl.style.height = 'auto'; | |
| queryEl.style.height = Math.min(queryEl.scrollHeight, 120) + 'px'; | |
| updateCharCounter(); | |
| }); | |
| function updateCharCounter() { | |
| const len = queryEl.value.length; | |
| charCounter.textContent = `${len} / 500`; | |
| charCounter.className = 'char-counter' + (len >= 500 ? ' max' : len >= 400 ? ' warn' : ''); | |
| } | |
| // Key shortcuts | |
| queryEl.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'N') { | |
| e.preventDefault(); | |
| startNewChat(); | |
| } | |
| if (e.key === 'Escape' && !sidebar.classList.contains('collapsed')) { | |
| toggleSidebar(); | |
| } | |
| }); | |
| // Scroll FAB | |
| messagesEl.addEventListener('scroll', () => { | |
| const distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight; | |
| scrollFab.classList.toggle('visible', distFromBottom > 150); | |
| }); | |
| function sendStarter(el) { queryEl.value = el.textContent; sendMessage(); } | |
| function sendSuggestion(el) { queryEl.value = el.textContent; sendMessage(); } | |
| function nowTime() { return new Date().toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }); } | |
| function scrollBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; } | |
| function removeTyping() { document.getElementById('typing')?.remove(); } | |
| // Append user bubble to UI | |
| function appendUserBubble(text, time) { | |
| if (document.getElementById('welcome')) { | |
| document.getElementById('welcome').remove(); | |
| } | |
| const row = document.createElement('div'); | |
| row.className = 'msg-row user'; | |
| row.innerHTML = ` | |
| <div class="bubble-wrap"> | |
| <div class="bubble user-b">${escapeHtml(text).replace(/\n/g,'<br>')}</div> | |
| <div class="msg-time">${time || nowTime()}</div> | |
| </div> | |
| <div class="avatar user-av"> | |
| <svg viewBox="0 0 24 24" fill="none" width="16" height="16"><circle cx="12" cy="8" r="4" stroke="#7a8190" stroke-width="2"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="#7a8190" stroke-width="2" stroke-linecap="round"/></svg> | |
| </div>`; | |
| messagesEl.insertBefore(row, scrollFab); | |
| scrollBottom(); | |
| } | |
| // Create action overlays | |
| function createActions(rawText, convId, msgIdx, existingFeedback) { | |
| const actions = document.createElement('div'); | |
| actions.className = 'msg-actions'; | |
| // Copy | |
| const copyBtn = document.createElement('button'); | |
| copyBtn.className = 'action-btn'; | |
| copyBtn.title = 'Copy'; | |
| copyBtn.innerHTML = ` | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> | |
| <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/> | |
| </svg> | |
| <span class="copy-toast">Copied!</span>`; | |
| copyBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| navigator.clipboard.writeText(rawText || '').then(() => { | |
| const toast = copyBtn.querySelector('.copy-toast'); | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 1500); | |
| }); | |
| }; | |
| actions.appendChild(copyBtn); | |
| // Thumbs up | |
| const upBtn = document.createElement('button'); | |
| upBtn.className = 'action-btn' + (existingFeedback === 'up' ? ' active-up' : ''); | |
| upBtn.title = 'Helpful'; | |
| upBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14z"/><path d="M7 22H4a2 2 0 01-2-2v-7a2 2 0 012-2h3"/></svg>`; | |
| upBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| const wasActive = upBtn.classList.contains('active-up'); | |
| upBtn.classList.toggle('active-up'); | |
| downBtn.classList.remove('active-down'); | |
| setFeedback(convId, msgIdx, wasActive ? null : 'up'); | |
| }; | |
| actions.appendChild(upBtn); | |
| // Thumbs down | |
| const downBtn = document.createElement('button'); | |
| downBtn.className = 'action-btn' + (existingFeedback === 'down' ? ' active-down' : ''); | |
| downBtn.title = 'Not helpful'; | |
| downBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10z"/><path d="M17 2h3a2 2 0 012 2v7a2 2 0 01-2 2h-3"/></svg>`; | |
| downBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| const wasActive = downBtn.classList.contains('active-down'); | |
| downBtn.classList.toggle('active-down'); | |
| upBtn.classList.remove('active-up'); | |
| setFeedback(convId, msgIdx, wasActive ? null : 'down'); | |
| }; | |
| actions.appendChild(downBtn); | |
| return actions; | |
| } | |
| // Submit lead captured details | |
| async function submitLeadForm(btn, convId, msgIdx) { | |
| const card = btn.closest('.lead-form-card'); | |
| const nameInput = card.querySelector('.lead-name-input'); | |
| const emailInput = card.querySelector('.lead-email-input'); | |
| const name = nameInput.value.trim(); | |
| const email = emailInput.value.trim(); | |
| if (!name) { | |
| alert("Please enter your name."); | |
| nameInput.focus(); | |
| return; | |
| } | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!email || !emailRegex.test(email)) { | |
| alert("Please enter a valid email address."); | |
| emailInput.focus(); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerText = "Submitting..."; | |
| const conv = getConversation(convId); | |
| let originalQuery = ""; | |
| if (conv && conv.messages[msgIdx - 1]) { | |
| originalQuery = conv.messages[msgIdx - 1].content; | |
| } | |
| try { | |
| const res = await fetch('/api/lead', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name: name, | |
| email: email, | |
| query: originalQuery, | |
| conversation_id: convId | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| if (conv && conv.messages[msgIdx]) { | |
| conv.messages[msgIdx].leadSubmitted = true; | |
| conv.messages[msgIdx].leadName = name; | |
| conv.messages[msgIdx].leadEmail = email; | |
| saveConversations(); | |
| } | |
| const successDiv = document.createElement('div'); | |
| successDiv.className = 'lead-success-msg'; | |
| successDiv.innerHTML = ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" style="color:var(--brand);"><polyline points="20 6 9 17 4 12"/></svg> | |
| Thank you, ${escapeHtml(name)}! We've received your details and our team will contact you at ${escapeHtml(email)} soon. | |
| `; | |
| card.replaceWith(successDiv); | |
| } else { | |
| alert("Failed to submit details: " + data.message); | |
| btn.disabled = false; | |
| btn.innerText = "Submit Details"; | |
| } | |
| } catch (e) { | |
| alert("Network error: " + e.message); | |
| btn.disabled = false; | |
| btn.innerText = "Submit Details"; | |
| } | |
| } | |
| // Append bot response | |
| function appendBotBubble(text, sources, suggestions, time, latency, msgIdx, convId, feedback, leadCapture, leadSubmitted, leadName, leadEmail) { | |
| const row = document.createElement('div'); | |
| row.className = 'msg-row'; | |
| const bwrap = document.createElement('div'); | |
| bwrap.className = 'bubble-wrap'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'bubble bot'; | |
| bubble.innerHTML = marked.parse(text || ''); | |
| row.innerHTML = `<div class="avatar bot"><img src="/static/Logomark.svg" alt="M" /></div>`; | |
| bwrap.appendChild(bubble); | |
| bwrap.appendChild(createActions(text, convId, msgIdx, feedback)); | |
| if (sources && sources.length) { | |
| const container = document.createElement('div'); | |
| container.className = 'sources-container'; | |
| const label = document.createElement('div'); | |
| label.className = 'sources-label'; | |
| label.innerText = 'For more details, visit:'; | |
| container.appendChild(label); | |
| const sr = document.createElement('div'); | |
| sr.className = 'sources-row'; | |
| sr.innerHTML = sources.map(s => | |
| `<a class="source-card" href="${s.url}" target="_blank" rel="noopener"> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="15 3 21 3 21 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><line x1="10" y1="14" x2="21" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> | |
| ${escapeHtml(s.title)}</a>` | |
| ).join(''); | |
| container.appendChild(sr); | |
| bwrap.appendChild(container); | |
| } | |
| // Lead Capture Form | |
| if (leadCapture) { | |
| if (leadSubmitted) { | |
| const successDiv = document.createElement('div'); | |
| successDiv.className = 'lead-success-msg'; | |
| successDiv.innerHTML = ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" style="color:var(--brand);"><polyline points="20 6 9 17 4 12"/></svg> | |
| Thank you, ${escapeHtml(leadName)}! We've received your details and our team will contact you at ${escapeHtml(leadEmail)} soon. | |
| `; | |
| bwrap.appendChild(successDiv); | |
| } else { | |
| const card = document.createElement('div'); | |
| card.className = 'lead-form-card'; | |
| card.innerHTML = ` | |
| <h3>Let's get in touch! π</h3> | |
| <p>Please share your details below so our business team can reach out to you with more information.</p> | |
| <div class="lead-form-group"> | |
| <input type="text" class="lead-input lead-name-input" placeholder="Your Name" /> | |
| <input type="email" class="lead-input lead-email-input" placeholder="Your Email" /> | |
| <button class="lead-submit-btn" onclick="submitLeadForm(this, '${convId}', ${msgIdx})">Submit Details</button> | |
| </div> | |
| `; | |
| bwrap.appendChild(card); | |
| } | |
| } | |
| if (suggestions && suggestions.length) { | |
| const sr = document.createElement('div'); | |
| sr.className = 'suggestions-row'; | |
| sr.innerHTML = suggestions.map(s => `<div class="suggestion-chip" onclick="sendSuggestion(this)">${escapeHtml(s)}</div>`).join(''); | |
| bwrap.appendChild(sr); | |
| } | |
| const footer = document.createElement('div'); | |
| footer.style.cssText = 'display:flex;gap:10px;align-items:center;'; | |
| const latencyHtml = latency > 0 | |
| ? `<div class="latency-badge"><svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/><polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>${latency}s</div>` | |
| : ''; | |
| footer.innerHTML = `<div class="msg-time">${time || nowTime()}</div>${latencyHtml}`; | |
| bwrap.appendChild(footer); | |
| row.appendChild(bwrap); | |
| messagesEl.insertBefore(row, scrollFab); | |
| scrollBottom(); | |
| } | |
| function showTyping() { | |
| const row = document.createElement('div'); | |
| row.className = 'typing-row'; row.id = 'typing'; | |
| row.innerHTML = ` | |
| <div class="avatar bot"><img src="/static/Logomark.svg" alt="M" /></div> | |
| <div class="typing-bubble"> | |
| <div class="dot"></div><div class="dot"></div><div class="dot"></div> | |
| </div>`; | |
| messagesEl.insertBefore(row, scrollFab); | |
| scrollBottom(); | |
| return row; | |
| } | |
| function appendBotError(text) { | |
| removeTyping(); | |
| const row = document.createElement('div'); | |
| row.className = 'msg-row'; | |
| row.innerHTML = ` | |
| <div class="avatar bot"><img src="/static/Logomark.svg" alt="M" /></div> | |
| <div class="bubble-wrap"> | |
| <div class="bubble bot">${marked.parse(text)}</div> | |
| <div class="msg-time">${nowTime()}</div> | |
| </div>`; | |
| messagesEl.insertBefore(row, scrollFab); | |
| scrollBottom(); | |
| } | |
| // ββ MAIN STREAM SEND βββββββββββββββββββββββββ | |
| async function sendMessage() { | |
| const query = queryEl.value.trim(); | |
| if (!query) return; | |
| if (isRateBlocked) return; | |
| if (isGenerating) { | |
| stopGenerating(); | |
| return; | |
| } | |
| // Anti-spam detection | |
| const now = Date.now(); | |
| messageTimestamps = messageTimestamps.filter(t => now - t < 10000); // last 10s window | |
| messageTimestamps.push(now); | |
| let isAbusing = false; | |
| if (messageTimestamps.length > 5) { | |
| isAbusing = true; | |
| } else if (messageTimestamps.length >= 2) { | |
| const lastDiff = messageTimestamps[messageTimestamps.length - 1] - messageTimestamps[messageTimestamps.length - 2]; | |
| if (lastDiff < 250) { // messages sent less than 250ms apart | |
| isAbusing = true; | |
| } | |
| } | |
| if (isAbusing) { | |
| const blockDuration = 5 * 60 * 1000; // 5 minutes | |
| const expiry = Date.now() + blockDuration; | |
| localStorage.setItem('chat_block_expiry', expiry.toString()); | |
| appendBotError("β οΈ **Rate Limit Exceeded!**\nYou are sending messages too quickly (possibly automated activity detected). To protect server resources, this chat session has been temporarily locked for **5 minutes**."); | |
| startBlockCountdown(expiry); | |
| return; | |
| } | |
| isGenerating = true; | |
| setButtonMode('stop'); | |
| // Create session if none active | |
| if (!currentConvId) { | |
| const conv = createConversation(); | |
| currentConvId = conv.id; | |
| } | |
| queryEl.value = ''; | |
| queryEl.style.height = 'auto'; | |
| updateCharCounter(); | |
| const userTime = nowTime(); | |
| appendUserBubble(query, userTime); | |
| addMessageToConv(currentConvId, { role: 'user', content: query, time: userTime }); | |
| const typingEl = showTyping(); | |
| hasStarted = false; | |
| let accumulatedText = ''; | |
| botRowEl = null; | |
| bubbleWrapEl = null; | |
| botBubbleEl = null; | |
| activeSources = null; | |
| activeQuery = query; | |
| let botMsgContent = ''; | |
| let botMsgSources = []; | |
| let botMsgSuggestions = []; | |
| let botMsgLatency = 0; | |
| let botMsgIdx = -1; | |
| currentAbortController = new AbortController(); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| query, | |
| history: getConversation(currentConvId) ? getConversation(currentConvId).messages : [] | |
| }), | |
| signal: currentAbortController.signal | |
| }); | |
| if (!response.ok) throw new Error(`Server error: ${response.status}`); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder('utf-8'); | |
| let buffer = ''; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed || !trimmed.startsWith('data: ')) continue; | |
| try { | |
| const data = JSON.parse(trimmed.slice(6)); | |
| if (data.event === 'sources') { | |
| activeSources = data.sources; | |
| botMsgSources = data.sources || []; | |
| } else if (data.event === 'token') { | |
| if (!hasStarted) { | |
| hasStarted = true; | |
| removeTyping(); | |
| botRowEl = document.createElement('div'); | |
| botRowEl.className = 'msg-row'; | |
| botRowEl.innerHTML = ` | |
| <div class="avatar bot"><img src="/static/Logomark.svg" alt="M" /></div> | |
| <div class="bubble-wrap"><div class="bubble bot"></div></div>`; | |
| messagesEl.insertBefore(botRowEl, scrollFab); | |
| bubbleWrapEl = botRowEl.querySelector('.bubble-wrap'); | |
| botBubbleEl = botRowEl.querySelector('.bubble.bot'); | |
| } | |
| accumulatedText += data.token; | |
| let displayText = accumulatedText; | |
| if (displayText.includes('[CAPTURE_LEAD]')) { | |
| displayText = displayText.replace('[CAPTURE_LEAD]', ''); | |
| } | |
| botBubbleEl.innerHTML = marked.parse(displayText); | |
| scrollBottom(); | |
| } else if (data.event === 'done') { | |
| let finalContent = data.text || accumulatedText; | |
| let leadCapture = false; | |
| if (finalContent.includes('[CAPTURE_LEAD]')) { | |
| finalContent = finalContent.replace('[CAPTURE_LEAD]', '').trim(); | |
| leadCapture = true; | |
| } | |
| botMsgContent = finalContent; | |
| botBubbleEl.innerHTML = marked.parse(botMsgContent); | |
| // Render sources only when streaming completes | |
| if (activeSources && activeSources.length && bubbleWrapEl) { | |
| const container = document.createElement('div'); | |
| container.className = 'sources-container'; | |
| const label = document.createElement('div'); | |
| label.className = 'sources-label'; | |
| label.innerText = 'For more details, visit:'; | |
| container.appendChild(label); | |
| const sr = document.createElement('div'); | |
| sr.className = 'sources-row'; | |
| sr.innerHTML = activeSources.map(s => | |
| `<a class="source-card" href="${s.url}" target="_blank" rel="noopener"> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="15 3 21 3 21 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><line x1="10" y1="14" x2="21" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> | |
| ${s.title}</a>` | |
| ).join(''); | |
| container.appendChild(sr); | |
| bubbleWrapEl.appendChild(container); | |
| } | |
| botMsgSuggestions = data.suggestions || []; | |
| botMsgLatency = data.latency || 0; | |
| // Save bot response to history | |
| const botTime = nowTime(); | |
| const conv = getConversation(currentConvId); | |
| botMsgIdx = conv ? conv.messages.length : -1; | |
| addMessageToConv(currentConvId, { | |
| role: 'bot', | |
| content: botMsgContent, | |
| sources: botMsgSources, | |
| suggestions: botMsgSuggestions, | |
| time: botTime, | |
| latency: botMsgLatency, | |
| feedback: null, | |
| leadCapture: leadCapture, | |
| leadSubmitted: false | |
| }); | |
| // Action buttons overlay | |
| if (bubbleWrapEl) { | |
| bubbleWrapEl.appendChild(createActions(botMsgContent, currentConvId, botMsgIdx, null)); | |
| } | |
| // Dynamic Lead capture form rendering | |
| if (leadCapture && bubbleWrapEl) { | |
| const card = document.createElement('div'); | |
| card.className = 'lead-form-card'; | |
| card.innerHTML = ` | |
| <h3>Let's get in touch! π</h3> | |
| <p>Please share your details below so our business team can reach out to you with more information.</p> | |
| <div class="lead-form-group"> | |
| <input type="text" class="lead-input lead-name-input" placeholder="Your Name" /> | |
| <input type="email" class="lead-input lead-email-input" placeholder="Your Email" /> | |
| <button class="lead-submit-btn" onclick="submitLeadForm(this, '${currentConvId}', ${botMsgIdx})">Submit Details</button> | |
| </div> | |
| `; | |
| bubbleWrapEl.appendChild(card); | |
| } | |
| // Suggestion chips | |
| if (botMsgSuggestions.length && bubbleWrapEl) { | |
| const sr = document.createElement('div'); | |
| sr.className = 'suggestions-row'; | |
| sr.innerHTML = botMsgSuggestions.map(s => `<div class="suggestion-chip" onclick="sendSuggestion(this)">${s}</div>`).join(''); | |
| bubbleWrapEl.appendChild(sr); | |
| } | |
| // Footer info | |
| if (bubbleWrapEl) { | |
| const footer = document.createElement('div'); | |
| footer.style.cssText = 'display:flex;gap:10px;align-items:center;'; | |
| const latencyHtml = botMsgLatency > 0 | |
| ? `<div class="latency-badge"><svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/><polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>${botMsgLatency}s</div>` | |
| : ''; | |
| footer.innerHTML = `<div class="msg-time">${botTime}</div>${latencyHtml}`; | |
| bubbleWrapEl.appendChild(footer); | |
| } | |
| scrollBottom(); | |
| } | |
| } catch (e) { | |
| console.error('SSE parse error:', e, trimmed); | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| if (err.name !== 'AbortError') { | |
| removeTyping(); | |
| if (!hasStarted) { | |
| appendBotError(`β οΈ Something went wrong. Please check if the server is running.\n\n\`${err.message}\``); | |
| } else { | |
| const errorDiv = document.createElement('div'); | |
| errorDiv.style.color = '#ff6b6b'; | |
| errorDiv.style.fontSize = '12px'; | |
| errorDiv.style.marginTop = '6px'; | |
| errorDiv.textContent = `β οΈ Stream error: ${err.message}`; | |
| bubbleWrapEl?.appendChild(errorDiv); | |
| scrollBottom(); | |
| } | |
| } | |
| } finally { | |
| isGenerating = false; | |
| setButtonMode('send'); | |
| queryEl.focus(); | |
| } | |
| } | |
| // ββ SPOTLIGHT TICKER ROTATION βββββββββββββββββββββββββ | |
| const tickerTextEl = document.getElementById('ticker-text'); | |
| let currentFactIdx = 0; | |
| function rotateTicker() { | |
| if (!tickerTextEl || !MINTOAK_FACTS.length) return; | |
| tickerTextEl.style.opacity = '0'; | |
| setTimeout(() => { | |
| currentFactIdx = (currentFactIdx + 1) % MINTOAK_FACTS.length; | |
| tickerTextEl.textContent = MINTOAK_FACTS[currentFactIdx]; | |
| tickerTextEl.style.opacity = '1'; | |
| }, 400); | |
| } | |
| setInterval(rotateTicker, 6000); | |
| // ββ RATE LIMIT COUNTDOWN βββββββββββββββββββββββββ | |
| function startBlockCountdown(expiry) { | |
| isRateBlocked = true; | |
| queryEl.disabled = true; | |
| sendBtn.disabled = true; | |
| const interval = setInterval(() => { | |
| const remaining = expiry - Date.now(); | |
| if (remaining <= 0) { | |
| clearInterval(interval); | |
| isRateBlocked = false; | |
| queryEl.disabled = false; | |
| queryEl.placeholder = "Ask me anything about Mintoakβ¦"; | |
| setButtonMode('send'); | |
| localStorage.removeItem('chat_block_expiry'); | |
| updateCharCounter(); | |
| } else { | |
| const mins = Math.floor(remaining / 60000); | |
| const secs = Math.floor((remaining % 60000) / 1000).toString().padStart(2, '0'); | |
| queryEl.placeholder = `Chat locked due to misuse β retry in ${mins}:${secs}`; | |
| sendBtn.disabled = true; | |
| } | |
| }, 1000); | |
| } | |
| function checkBlockStatus() { | |
| const expiry = localStorage.getItem('chat_block_expiry'); | |
| if (expiry && Date.now() < parseInt(expiry)) { | |
| startBlockCountdown(parseInt(expiry)); | |
| } | |
| } | |
| // ββ INITIALIZATION βββββββββββββββββββββββββ | |
| loadConversations(); | |
| renderSidebar(); | |
| updateCharCounter(); | |
| checkBlockStatus(); | |
| </script> | |
| </body> | |
| </html> | |