Rutvij07's picture
Update assistant persona to Mintoak Website Assistant, add evaluation scripts and update data
5fc8a03
Raw
History Blame Contribute Delete
70.4 kB
<!DOCTYPE html>
<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) !important;
color: var(--brand) !important;
background: var(--brand-soft) !important;
}
.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 &amp; 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 &nbsp;Β·&nbsp; <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>