AI_Chatbot / chat.html
Hamza-Naimat's picture
widget+chat: show leadbox at most once per session, stop after dismissal
1e3d37f
Raw
History Blame Contribute Delete
32.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Support Chat</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<style>
:root {
--primary: #6366f1;
--secondary: #4f46e5;
--gradient: linear-gradient(135deg, var(--primary), var(--secondary));
--bubble-user: var(--gradient);
--bubble-bot: #f1f5f9;
--font-family: 'Inter', -apple-system, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-family);
background: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.chat-container {
width: min(450px, 95vw);
height: min(750px, 92vh);
background: white;
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.chat-header {
background: var(--gradient);
color: white;
padding: 24px 20px;
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.avatar {
width: 48px; height: 48px;
background: rgba(255,255,255,0.25);
border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 24px;
}
.info h3 { font-size: 16px; font-weight: 700; letter-spacing: -0.01em; }
.info p { font-size: 13px; opacity: 0.9; margin-top: 2px; display: flex; align-items: center; gap: 5px; }
.online-dot { width: 8px; height: 8px; background: #4ade80; border-radius: 50%; }
.new-chat-btn {
margin-left: auto;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
color: white;
font-size: 12px;
padding: 6px 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.new-chat-btn:hover { background: rgba(255,255,255,0.25); }
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
background: #ffffff;
}
.message { display: flex; flex-direction: column; gap: 6px; max-width: 85%; }
.message.bot { align-self: flex-start; }
.message.user { align-self: flex-end; }
.bubble {
padding: 12px 18px;
border-radius: 20px;
font-size: 14.5px;
line-height: 1.6;
position: relative;
}
.message.bot .bubble { background: var(--bubble-bot); color: #1e293b; border-bottom-left-radius: 4px; }
.message.user .bubble { background: var(--bubble-user); color: white; border-bottom-right-radius: 4px; }
/* Markdown overrides */
.bubble h3 { font-size: 16px; margin: 12px 0 8px; color: var(--primary); }
.bubble ul, .bubble ol { margin-left: 20px; margin-bottom: 10px; padding-left: 4px; }
.bubble code { background: rgba(0,0,0,0.05); padding: 2px 4px; border-radius: 4px; font-family: monospace; }
.bubble pre { background: #1e293b; color: #f8fafc; padding: 12px; border-radius: 12px; margin: 10px 0; overflow-x: auto; }
/* Typing indicator */
.typing-indicator {
display: flex; align-items: center; gap: 4px;
padding: 14px 18px;
background: var(--bubble-bot);
border-radius: 20px;
border-bottom-left-radius: 4px;
width: fit-content;
}
.typing-indicator span {
width: 7px; height: 7px;
background: #94a3b8;
border-radius: 50%;
display: inline-block;
animation: bounce 1.2s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
/* Feedback buttons */
.feedback-row {
display: flex;
gap: 6px;
margin-top: 2px;
opacity: 0;
transition: opacity 0.2s;
}
.message.bot:hover .feedback-row { opacity: 1; }
.feedback-btn {
background: none;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 3px 8px;
font-size: 13px;
cursor: pointer;
color: #94a3b8;
transition: all 0.15s;
line-height: 1.4;
}
.feedback-btn:hover { background: #f1f5f9; color: #475569; }
.feedback-btn.voted-up { background: #dcfce7; border-color: #86efac; color: #16a34a; }
.feedback-btn.voted-down { background: #fee2e2; border-color: #fca5a5; color: #dc2626; }
/* Suggestion chips */
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
max-width: 340px;
}
.suggestion-chip {
background: white;
border: 1.5px solid #e2e8f0;
border-radius: 20px;
padding: 6px 14px;
font-size: 12.5px;
color: #475569;
cursor: pointer;
transition: all 0.15s;
font-family: var(--font-family);
}
.suggestion-chip:hover {
border-color: var(--primary);
color: var(--primary);
background: #f5f3ff;
}
/* Lead card */
.lead-card {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 20px;
padding: 20px;
margin: 10px 0;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.lead-card p { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 14px; }
.lead-form { display: flex; flex-direction: column; gap: 8px; }
.lead-input {
width: 100%; padding: 10px; border: 1.5px solid #e2e8f0; border-radius: 10px;
font-size: 13px; font-family: var(--font-family); transition: border-color 0.2s;
}
.lead-input:focus { outline: none; border-color: var(--primary); }
.lead-btn-group { display: flex; flex-direction: row; gap: 8px; flex-wrap: wrap; }
.lead-btn {
flex: 1; min-width: 100px; padding: 11px 6px;
border-radius: 12px; border: none;
font-weight: 700; font-size: 12px;
cursor: pointer; transition: all 0.2s;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.btn-yes { background: #6366f1; color: white; }
.btn-no { background: #f1f5f9; color: #64748b; }
.btn-submit { background: var(--gradient); color: white; }
.btn-email { background: #6366f1; color: white; }
.btn-whatsapp { background: #22c55e; color: white; }
.btn-cancel { background: #f1f5f9; color: #64748b; }
.lead-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
/* Input area */
.input-area {
padding: 20px;
border-top: 1px solid #f1f5f9;
display: flex; gap: 12px; align-items: center;
background: white;
}
.input-area input {
flex: 1;
border: 1.5px solid #e2e8f0;
border-radius: 16px;
padding: 12px 20px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
font-family: var(--font-family);
}
.input-area input:focus { border-color: var(--primary); }
.send-btn {
width: 48px; height: 48px;
background: var(--gradient);
border: none; border-radius: 16px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
color: white; transition: all 0.2s;
}
.send-btn:hover { transform: scale(1.05); }
.send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.source-citations { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(0,0,0,0.06); display: flex; flex-wrap: wrap; gap: 4px; }
.cite-badge { display: inline-block; font-size: 11px; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 10px; padding: 2px 8px; color: #6366f1; text-decoration: none; }
.cite-badge:hover { background: #e0e7ff; }
.handoff-card { background: #f0fdf4; border: 2px solid #86efac; border-radius: 20px; padding: 18px; margin: 10px 0; animation: slideUp 0.3s ease-out; }
.handoff-card p { font-size: 14px; font-weight: 600; color: #166534; margin-bottom: 12px; }
.handoff-input { width:100%; padding:9px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:13px; margin-bottom:8px; box-sizing:border-box; }
.btn-handoff { background:#22c55e; color:white; border:none; border-radius:12px; padding:10px 20px; cursor:pointer; font-weight:600; font-size:13px; }
.btn-handoff-cancel { background:#f1f5f9; color:#475569; border:1px solid #e2e8f0; border-radius:12px; padding:10px 16px; cursor:pointer; font-size:13px; }
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<div class="avatar" id="header-avatar">πŸ€–</div>
<div class="info">
<h3 id="header-title">AI Support</h3>
<p><span class="online-dot"></span> <span id="header-subtitle">Ready to assist</span></p>
</div>
<button class="new-chat-btn" onclick="newChat()">New Chat</button>
</div>
<div class="messages" id="messages">
<div class="message bot">
<div class="bubble" id="welcome-msg">Hi! How can I help you today?</div>
</div>
</div>
<div class="input-area">
<input type="text" id="user-input" placeholder="Message..." autocomplete="off" />
<button class="send-btn" id="send-btn" onclick="sendMessage()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
</div>
<script>
// XSS protection: strip raw HTML blocks from LLM output β€” keep markdown formatting only
(function() {
const renderer = new marked.Renderer();
renderer.html = () => ''; // drop any raw <script>, <img onerror=...> etc from LLM
marked.use({ renderer, breaks: true, gfm: true });
})();
const API_URL = (window.location.origin.includes('localhost') || window.location.origin.includes('127.0.0.1'))
? 'http://127.0.0.1:8000' : window.location.origin;
const messagesEl = document.getElementById("messages");
const inputEl = document.getElementById("user-input");
const sendBtn = document.getElementById("send-btn");
const HANDOFF_RE = /\b(talk to|speak to|connect me with|live agent|real person|human support|call me|speak with someone|agent please|need help from)\b/i;
// Session β€” reset if active DB has changed
let sessionId = localStorage.getItem("chat_session_id");
if (!sessionId) {
sessionId = crypto.randomUUID();
localStorage.setItem("chat_session_id", sessionId);
}
// Restore history from localStorage so conversation survives page reloads
let HISTORY_KEY = "chat_history_" + sessionId;
// Clear chat if DB switched since last visit
fetch(`${API_URL}/health`).then(r => r.json()).then(h => {
const currentDb = h.db || "";
const lastDb = localStorage.getItem("last_active_db") || "";
if (currentDb && lastDb && currentDb !== lastDb) {
// DB changed β€” wipe old session
localStorage.removeItem(HISTORY_KEY);
sessionId = crypto.randomUUID();
localStorage.setItem("chat_session_id", sessionId);
HISTORY_KEY = "chat_history_" + sessionId;
history.length = 0;
document.getElementById("messages").innerHTML = "";
}
if (currentDb) localStorage.setItem("last_active_db", currentDb);
}).catch(() => {});
const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]");
let widgetKey = "";
let contactEmail = "support@agentfactory.com";
let contactWhatsApp = "";
// Auto-clear chat when active bot changes (e.g. admin switches DB)
let _activeBotName = "";
function _checkBotChanged() {
fetch(`${API_URL}/config`).then(r => r.json()).then(cfg => {
const name = cfg.bot_name || "";
if (_activeBotName && name && name !== _activeBotName) {
localStorage.setItem("chat_session_id", crypto.randomUUID());
window.location.reload();
}
_activeBotName = name || _activeBotName;
}).catch(() => {});
}
document.addEventListener("visibilitychange", () => { if (!document.hidden) _checkBotChanged(); });
window.addEventListener("focus", _checkBotChanged);
// No idle polling: on scale-to-zero hosting every request keeps the
// container awake (= billed). Bot-change detection happens on tab
// focus/visibility and before each send instead.
// Load branding config
fetch(`${API_URL}/config`).then(r => r.json()).then(cfg => {
widgetKey = cfg.widget_key;
_activeBotName = cfg.bot_name || "";
contactEmail = cfg.contact_email;
contactWhatsApp = cfg.whatsapp_number;
const b = cfg.branding || {};
const p = b.primary_color || cfg.primary_color || "#6366f1";
const s = b.secondary_color || cfg.secondary_color || "#4f46e5";
document.documentElement.style.setProperty("--primary", p);
document.documentElement.style.setProperty("--secondary", s);
document.documentElement.style.setProperty("--gradient", `linear-gradient(135deg, ${p}, ${s})`);
if (b.bot_bubble_color || cfg.bot_bubble_color) document.documentElement.style.setProperty("--bubble-bot", b.bot_bubble_color || cfg.bot_bubble_color);
if (b.user_bubble_color || cfg.user_bubble_color) document.documentElement.style.setProperty("--bubble-user", b.user_bubble_color || cfg.user_bubble_color);
document.getElementById("header-title").textContent = cfg.header_title || b.header_title || "Support";
document.getElementById("header-subtitle").textContent = cfg.header_subtitle || b.header_subtitle || "Ready to assist";
document.getElementById("welcome-msg").textContent = cfg.welcome_message || b.welcome_message || "Hi! How can I help you today?";
if (cfg.logo_emoji || b.logo_emoji) document.getElementById("header-avatar").textContent = cfg.logo_emoji || b.logo_emoji;
}).catch(() => {});
// Restore previous messages from localStorage
if (history.length > 0) {
messagesEl.innerHTML = "";
for (let i = 0; i < history.length; i++) {
const m = history[i];
const div = document.createElement("div");
div.className = "message " + (m.role === "user" ? "user" : "bot");
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = m.content;
div.appendChild(bubble);
messagesEl.appendChild(div);
}
messagesEl.scrollTop = messagesEl.scrollHeight;
}
inputEl.addEventListener("keypress", e => { if (e.key === "Enter") sendMessage(); });
async function sendMessage() {
const q = inputEl.value.trim();
if (!q) return;
resetProactiveTimer();
// Human handoff detection
if (HANDOFF_RE.test(q)) {
addMessage(q, "user");
inputEl.value = "";
showHandoffCard();
return;
}
addMessage(q, "user");
inputEl.value = "";
sendBtn.disabled = true;
// Show typing indicator
const typingId = "typing-" + Date.now();
const typingDiv = document.createElement("div");
typingDiv.className = "message bot";
typingDiv.id = typingId;
typingDiv.innerHTML = `<div class="typing-indicator"><span></span><span></span><span></span></div>`;
messagesEl.appendChild(typingDiv);
messagesEl.scrollTop = messagesEl.scrollHeight;
const botMsgId = "bot-" + Date.now();
let bubble = null;
let fullAnswer = "";
let lastQuestion = q;
try {
const response = await fetch(`${API_URL}/chat`, {
method: "POST",
headers: Object.assign({"Content-Type": "application/json"}, widgetKey ? {"X-Widget-Key": widgetKey} : {}),
body: JSON.stringify({ question: q, history, session_id: sessionId,
visitor_id: sessionId, page_url: location.href, page_title: document.title }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
let data;
try { data = JSON.parse(line.substring(6)); } catch { continue; }
if (data.type === "reset") {
// Server is replacing live-streamed text (a validator rewrote the answer,
// or a failed LLM attempt is being retried) β€” clear and rebuild from next chunk.
fullAnswer = "";
if (bubble) bubble.innerHTML = "";
continue;
}
if (data.type === "chunk") {
// Remove typing indicator on first chunk
if (!bubble) {
document.getElementById(typingId)?.remove();
const msgDiv = document.createElement("div");
msgDiv.className = "message bot";
msgDiv.id = botMsgId;
msgDiv.innerHTML = `<div class="bubble"></div>`;
messagesEl.appendChild(msgDiv);
bubble = msgDiv.querySelector(".bubble");
// Scroll to top of new bot message so user reads from the start
msgDiv.scrollIntoView({ block: 'start', behavior: 'smooth' });
fullAnswer += data.content;
bubble.innerHTML = DOMPurify.sanitize(marked.parse(renderCards(fullAnswer)));
// Skip nearBottom on first chunk β€” scrollIntoView already positioned correctly
continue;
}
fullAnswer += data.content;
bubble.innerHTML = DOMPurify.sanitize(marked.parse(renderCards(fullAnswer)));
// Only auto-scroll during streaming if user hasn't scrolled up to read
const nearBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < 80;
if (nearBottom) messagesEl.scrollTop = messagesEl.scrollHeight;
} else if (data.type === "metadata") {
if (data.capture_lead) showLeadBox();
if (data.sources && data.sources.length > 0) {
const srcDiv = document.createElement("div");
srcDiv.className = "source-citations";
srcDiv.innerHTML = data.sources.map((s, i) => {
try {
const u = new URL(s);
// Block non-http schemes (javascript:, data:, etc.)
if (!['http:', 'https:'].includes(u.protocol)) return '';
const seg = u.pathname.split('/').filter(Boolean).pop() || 'source';
const safeUrl = u.href.replace(/"/g, '&quot;');
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="cite-badge">[${i+1}] ${seg}</a>`;
} catch { return ''; }
}).filter(Boolean).join('');
if (bubble) bubble.appendChild(srcDiv);
}
if (data.options && data.options.length) renderSuggestions(data.options, botMsgId);
} else if (data.type === "suggestions") {
renderSuggestions(data.items, botMsgId);
}
}
}
// Append feedback row to bot message
if (bubble) {
renderFeedbackRow(botMsgId, q, fullAnswer);
}
resetProactiveTimer();
history.push({ role: "user", content: q }, { role: "assistant", content: fullAnswer });
// Keep last 20 messages (10 turns) to avoid unbounded growth
if (history.length > 20) history.splice(0, history.length - 20);
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
} catch (e) {
document.getElementById(typingId)?.remove();
addMessage("Error connecting to server.", "bot");
}
sendBtn.disabled = false;
inputEl.focus();
checkCsatTrigger();
}
// --- Proactive idle trigger ---
let proactiveSent = false;
let proactiveTimer = null;
function resetProactiveTimer() {
clearTimeout(proactiveTimer);
if (proactiveSent) return;
proactiveTimer = setTimeout(() => {
if (proactiveSent) return;
proactiveSent = true;
addMessage("Still there? Feel free to ask me anything β€” I'm here to help!", "bot");
}, 45000);
}
resetProactiveTimer();
// --- Human Handoff ---
function showHandoffCard() {
const cardId = "handoff-" + Date.now();
const div = document.createElement("div");
div.className = "message bot";
div.id = cardId;
div.innerHTML = `<div class="handoff-card">
<p>I'll connect you with our team right away!</p>
<input class="handoff-input" id="hf-name" placeholder="Your name (optional)" />
<input class="handoff-input" id="hf-contact" placeholder="Your email or WhatsApp (optional)" />
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn-handoff" onclick="submitHandoff('${cardId}')">Send my conversation to the team</button>
<button class="btn-handoff-cancel" onclick="document.getElementById('${cardId}').remove()">Continue chatting</button>
</div>
</div>`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
async function submitHandoff(cardId) {
const name = document.getElementById("hf-name")?.value.trim() || "Website Visitor";
const contact = document.getElementById("hf-contact")?.value.trim() || "";
const card = document.getElementById(cardId);
const btn = card?.querySelector(".btn-handoff");
if (btn) btn.textContent = "Sending...";
try {
const r = await fetch(`${API_URL}/handoff`, {
method: "POST",
headers: Object.assign({"Content-Type":"application/json"}, widgetKey ? {"X-Widget-Key":widgetKey} : {}),
body: JSON.stringify({session_id: sessionId, conversation: history, name, contact_info: contact})
});
const data = await r.json();
const hc = card?.querySelector(".handoff-card");
if (hc) hc.innerHTML = `<p style="color:#166534">${data.message || "Team notified!"}</p>`;
} catch {
const hc = card?.querySelector(".handoff-card");
if (hc) hc.innerHTML = `<p style="color:#dc2626">Could not send. Please contact us directly.</p>`;
}
}
function addMessage(html, role, id) {
const div = document.createElement("div");
div.className = `message ${role}`;
if (id) div.id = id;
div.innerHTML = `<div class="bubble">${html}</div>`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function renderFeedbackRow(msgId, question, answer) {
const msgDiv = document.getElementById(msgId);
if (!msgDiv) return;
const row = document.createElement("div");
row.className = "feedback-row";
row.id = "fb-" + msgId;
row.innerHTML = `
<button class="feedback-btn" title="Helpful" onclick="submitFeedback(1, '${msgId}', this)">πŸ‘</button>
<button class="feedback-btn" title="Not helpful" onclick="submitFeedback(-1, '${msgId}', this)">πŸ‘Ž</button>
`;
// Store question+answer in dataset for the feedback call
row.dataset.question = question;
row.dataset.answer = answer.substring(0, 400);
msgDiv.appendChild(row);
}
async function submitFeedback(rating, msgId, btn) {
const row = document.getElementById("fb-" + msgId);
const question = row?.dataset.question || "";
const answer = row?.dataset.answer || "";
try {
await fetch(`${API_URL}/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId, rating, question, answer }),
});
// Visual feedback
btn.classList.add(rating === 1 ? "voted-up" : "voted-down");
btn.disabled = true;
const sibling = btn.parentElement.querySelector(`.feedback-btn:not(.voted-up):not(.voted-down)`);
if (sibling) sibling.disabled = true;
setTimeout(() => { if (row) row.style.display = "none"; }, 1500);
} catch (e) { /* silent */ }
}
function renderSuggestions(items, afterMsgId) {
if (!items || !items.length) return;
const msgDiv = document.getElementById(afterMsgId);
if (!msgDiv) return;
const container = document.createElement("div");
container.className = "suggestions";
items.forEach(text => {
const chip = document.createElement("button");
chip.className = "suggestion-chip";
chip.textContent = text;
chip.onclick = () => {
inputEl.value = text;
container.remove();
sendMessage();
};
container.appendChild(chip);
});
msgDiv.appendChild(container);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function showLeadOptions(cardId) {
document.getElementById(cardId).innerHTML = `
<p>Would you like to connect with our team?</p>
<div class="lead-btn-group">
<button class="lead-btn btn-yes" onclick="showLeadEmailForm('${cardId}')">βœ… Yes, connect me</button>
<button class="lead-btn btn-no" onclick="dismissLeadBox('${cardId}')">βœ• No thanks</button>
</div>
`;
}
function showLeadEmailForm(cardId) {
document.getElementById(cardId).innerHTML = `
<div class="lead-form">
<p>Drop your details and we'll get back to you!</p>
<input type="text" id="lead-name-${cardId}" placeholder="Your Name" class="lead-input">
<input type="email" id="lead-email-${cardId}" placeholder="Email Address" class="lead-input">
<input type="tel" id="lead-phone-${cardId}" placeholder="Phone / WhatsApp Number" class="lead-input">
<textarea id="lead-msg-${cardId}" placeholder="How can we help?" class="lead-input" rows="2"></textarea>
<div class="lead-btn-group">
<button class="lead-btn btn-submit" onclick="submitLeadForm('${cardId}')">Send Details</button>
<button class="lead-btn btn-cancel" onclick="showLeadOptions('${cardId}')">← Back</button>
</div>
</div>
`;
}
async function submitLeadForm(cardId) {
const name = document.getElementById(`lead-name-${cardId}`).value;
const email = document.getElementById(`lead-email-${cardId}`).value;
const phone = document.getElementById(`lead-phone-${cardId}`)?.value || "";
const msg = document.getElementById(`lead-msg-${cardId}`).value;
if(!name || !email) {
alert("Please provide at least your name and email.");
return;
}
const btn = document.querySelector(`#${cardId} .btn-submit`);
btn.disabled = true;
btn.innerText = "Sending...";
try {
await fetch(`${API_URL}/submit-lead`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, whatsapp: phone, message: msg, session_id: sessionId })
});
document.getElementById(cardId).innerHTML = `
<div style="text-align:center;color:#16a34a;padding:10px;">
<p>βœ… <b>Sent!</b></p>
<p style="font-size:12px;">We will contact you shortly.</p>
</div>
`;
setTimeout(() => document.getElementById(cardId)?.remove(), 5000);
} catch(e) {
btn.disabled = false;
btn.innerText = "Retry";
alert("Error sending details. Please try again.");
}
}
function leadSessionKey(type) {
return `lead_${type}_${sessionId}`;
}
function dismissLeadBox(cardId) {
sessionStorage.setItem(leadSessionKey("dismissed"), "1");
document.getElementById(cardId)?.remove();
}
function showLeadBox() {
if (
sessionStorage.getItem(leadSessionKey("shown")) === "1" ||
sessionStorage.getItem(leadSessionKey("dismissed")) === "1" ||
document.querySelector(".lead-card")
) return;
sessionStorage.setItem(leadSessionKey("shown"), "1");
const cardId = "lead-" + Date.now();
const div = document.createElement("div");
div.className = "lead-card";
div.id = cardId;
div.innerHTML = `
<p>Would you like to connect with our team?</p>
<div class="lead-btn-group">
<button class="lead-btn btn-yes" onclick="showLeadEmailForm('${cardId}')">βœ… Yes, connect me</button>
<button class="lead-btn btn-no" onclick="dismissLeadBox('${cardId}')">βœ• No thanks</button>
</div>
`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function newChat() {
localStorage.removeItem(HISTORY_KEY);
history.length = 0;
sessionId = crypto.randomUUID();
localStorage.setItem("chat_session_id", sessionId);
HISTORY_KEY = "chat_history_" + sessionId;
messagesEl.innerHTML = '<div class="message bot"><div class="bubble">Hi! How can I help you today?</div></div>';
clearTimeout(csatTimer);
proactiveSent = false;
resetProactiveTimer();
}
// ── Rich card renderer ─────────────────────────────────────────────────────
function renderCards(text) {
return text.replace(/\[CARD\](.*?)\[\/CARD\]/gs, (_, inner) => {
const [title, desc, url] = inner.split('|').map(s => s.trim());
const link = url ? `<a href="${url}" target="_blank" style="font-size:12px;color:#6366f1;">View β†’</a>` : '';
return `<div style="border:1px solid #e2e8f0;border-radius:12px;padding:10px 14px;margin:6px 0;background:#f8fafc;">
<b>${title || ''}</b>${desc ? `<br><span style="font-size:13px;color:#64748b;">${desc}</span>` : ''}${link ? `<br>${link}` : ''}
</div>`;
});
}
// ── CSAT survey (fires after 5th question, once per session) ─────────────
let csatShown = false;
let csatQuestionCount = 0;
function checkCsatTrigger() {
if (csatShown) return;
csatQuestionCount++;
if (csatQuestionCount === 5) showCsat();
}
function showCsat() {
if (csatShown) return;
csatShown = true;
const div = document.createElement("div");
div.className = "message bot";
div.style.alignSelf = "center";
div.innerHTML = `
<div class="bubble" style="background:#fafafa;border:1px solid #e2e8f0;text-align:center;padding:16px 20px;">
<p style="margin:0 0 12px;font-weight:600;font-size:14px;color:#1e293b;">How would you rate your experience?</p>
<div id="csat-stars" style="display:flex;justify-content:center;gap:4px;">
${[1,2,3,4,5].map(n => `
<span class="csat-star"
data-val="${n}"
onmouseover="highlightStars(${n})"
onmouseout="highlightStars(0)"
onclick="submitCsat(${n},this)"
style="font-size:28px;cursor:pointer;color:#cbd5e1;line-height:1;user-select:none;">&#9733;</span>
`).join('')}
</div>
<p style="margin:8px 0 0;font-size:12px;color:#94a3b8;">Press 1–5 to rate</p>
</div>`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
// Keyboard shortcut: 1-5
const keyHandler = (e) => {
if (['1','2','3','4','5'].includes(e.key)) {
const stars = document.querySelectorAll('.csat-star');
if (stars.length) { submitCsat(parseInt(e.key), stars[parseInt(e.key)-1]); }
document.removeEventListener('keydown', keyHandler);
}
};
document.addEventListener('keydown', keyHandler);
}
function highlightStars(n) {
document.querySelectorAll('.csat-star').forEach(s => {
s.style.color = parseInt(s.dataset.val) <= n ? '#f59e0b' : '#cbd5e1';
});
}
async function submitCsat(rating, btn) {
const bubble = btn.closest ? btn.closest('.bubble') : btn.parentElement.closest('.bubble');
bubble.innerHTML = `
<div style="text-align:center;padding:4px 0;">
<div style="display:flex;justify-content:center;gap:2px;margin-bottom:8px;">
${[1,2,3,4,5].map(n => `<span style="font-size:24px;color:${n<=rating?'#f59e0b':'#cbd5e1'};">&#9733;</span>`).join('')}
</div>
<p style="margin:0;font-weight:600;font-size:14px;color:#1e293b;">Thank you for your feedback.</p>
</div>`;
try {
await fetch(`${API_URL}/csat`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ rating, session_id: sessionId })
});
} catch {}
}
</script>
</body>
</html>