chatbot21 / static /addon.html
Martechsol
Fix: notification sound auto-login and suspend issues
f6d977d
Raw
History Blame Contribute Delete
46 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Martechsol Assistant Widget</title>
<style>
/* Premium Design for Martechsol Assistant */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
#martech-chat-wrapper {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Floating Action Button (FAB) */
#martech-chat-fab {
position: fixed;
right: 10px;
bottom: 25px;
width: auto;
height: auto;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999999;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
-webkit-tap-highlight-color: transparent;
}
#martech-chat-fab:hover {
transform: scale(1.1);
}
#martech-chat-fab.is-active {
background: #1e293b;
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
transform: scale(0.8);
}
/* Chat Window Styling */
#martech-chat-box {
position: fixed;
right: 25px;
bottom: 100px;
width: 400px;
height: 550px;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
display: none;
flex-direction: column;
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
z-index: 999998;
border: 1px solid #e2e8f0;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#martech-chat-box.is-visible {
display: flex;
opacity: 1;
transform: translateY(0);
}
#martech-chat-header {
background: #ffffff;
padding: 16px 20px;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
gap: 12px;
}
@keyframes pulse-dot {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
#martech-chat-header .dot {
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
animation: pulse-dot 2s infinite;
transition: background 0.3s;
}
#martech-chat-header span {
font-weight: 600;
color: #1e293b;
font-size: 15px;
padding-left: 20px;
}
.header-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.btn-end-chat {
background: #fee2e2;
color: #ef4444;
border: none;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-end-chat:hover {
background: #fecaca;
}
#rating-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
#rating-overlay.show {
opacity: 1;
visibility: visible;
}
.stars {
display: flex;
gap: 8px;
font-size: 32px;
color: #cbd5e1;
cursor: pointer;
margin: 16px 0;
}
.stars span {
transition: color 0.2s;
}
.stars span:hover,
.stars span.active {
color: #f59e0b;
}
/* Custom Chat Area Styling */
#chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: #f8fafc;
scroll-behavior: smooth;
}
/* Custom Scrollbar for messages */
#chat-messages::-webkit-scrollbar {
width: 6px;
}
#chat-messages::-webkit-scrollbar-track {
background: transparent;
}
#chat-messages::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
.chat-message {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.chat-message.user {
align-self: flex-end;
background: #1e293b;
color: white;
border-bottom-right-radius: 4px;
}
.chat-message.bot {
align-self: flex-start;
background: #ffffff;
color: #334155;
border-bottom-left-radius: 4px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
/* Surgical Bolding & List Style */
.chat-message b {
font-weight: 800;
color: #0f172a;
}
.chat-message.user b {
color: #ffffff;
}
.queue-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
justify-content: center;
}
.btn-queue {
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn-remind {
background: #fff;
border-color: #f59e0b;
color: #d97706;
}
.btn-remind:hover:not(:disabled) {
background: #fef3c7;
}
.btn-remind:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: #cbd5e1;
color: #94a3b8;
}
.btn-exit-queue {
background: #fff;
border-color: #e2e8f0;
color: #475569;
}
.btn-exit-queue:hover {
background: #f1f5f9;
}
#chat-input-area {
padding: 16px;
background: #ffffff;
border-top: 1px solid #f1f5f9;
display: flex;
gap: 8px;
align-items: center;
}
#chat-input {
flex: 1;
border: 1px solid #e2e8f0;
border-radius: 20px;
padding: 12px 16px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
font-family: inherit;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
}
#chat-input:focus {
border-color: #10b981;
}
#chat-input:disabled {
background: #f1f5f9;
cursor: not-allowed;
}
#chat-send {
background: #10b981;
color: white;
border: none;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
#chat-send:hover {
background: #059669;
}
#chat-send:active {
transform: scale(0.95);
}
#chat-send:disabled {
background: #cbd5e1;
cursor: not-allowed;
transform: none;
}
/* Loading / Typing Animation */
.typing-indicator {
display: flex;
gap: 4px;
padding: 4px 2px;
align-items: center;
height: 18px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: #94a3b8;
border-radius: 50%;
animation: typing-dots 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing-dots {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@media (max-width: 480px) {
#martech-chat-box {
right: 15px;
bottom: 100px;
width: calc(100vw - 30px);
height: 70vh;
}
}
</style>
</head>
<body>
<!-- Lottie Player Script -->
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
<div id="martech-chat-wrapper">
<div id="martech-chat-fab">
<!-- Animated Chatbot Icon -->
<div id="chat-icon"
style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
<dotlottie-player src="https://joedown11-chatbot21.hf.space/static/bot-icon.lottie"
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop
autoplay>
</dotlottie-player>
</div>
<!-- Close Icon -->
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
fill="white" />
</svg>
</div>
<div id="martech-chat-box">
<div id="martech-chat-header" style="display:flex; justify-content:space-between; align-items:center;">
<div style="display:flex; align-items:center;">
<div class="dot" id="chat-status-dot"></div>
<span id="chat-header-title">Martechsol Assistant</span>
</div>
</div>
<!-- Custom Chat UI replacing iframe -->
<div id="chat-messages">
<div class="chat-message bot">Welcome! How can I help you today?</div>
</div>
<div id="queue-actions-area" class="queue-actions" style="display: none;">
<button id="btn-remind" class="btn-queue btn-remind" disabled>
🔔 <span id="remind-text">Wait 3:00</span>
</button>
<button id="btn-exit-queue" class="btn-queue btn-exit-queue">
✖ Exit Queue
</button>
</div>
<div id="chat-input-area">
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
<button id="chat-send" title="Send">
<!-- Send Icon -->
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div id="rating-overlay">
<h3 style="color:#1e293b; margin-bottom:8px">Rate Your Experience</h3>
<p style="color:#64748b; font-size:14px">How was your chat?</p>
<div class="stars" id="rating-stars">
<span data-val="1"></span><span data-val="2"></span><span data-val="3"></span><span
data-val="4"></span><span data-val="5"></span>
</div>
<button id="btn-submit-rating"
style="background:#10b981; color:#fff; border:none; padding:10px 24px; border-radius:8px; font-weight:600; cursor:pointer; margin-top:8px">Submit
& Close</button>
</div>
</div>
</div>
<script>
(function () {
// UI Elements
const fab = document.getElementById('martech-chat-fab');
const box = document.getElementById('martech-chat-box');
const chatIcon = document.getElementById('chat-icon');
const closeIcon = document.getElementById('close-icon');
const messagesContainer = document.getElementById('chat-messages');
const inputField = document.getElementById('chat-input');
const sendButton = document.getElementById('chat-send');
// FAB Toggle Logic
fab.addEventListener('click', function () {
const isVisible = box.classList.contains('is-visible');
if (isVisible) {
box.classList.remove('is-visible');
setTimeout(() => { box.style.display = 'none'; }, 300);
chatIcon.style.display = 'block';
closeIcon.style.display = 'none';
fab.classList.remove('is-active');
} else {
box.style.display = 'flex';
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
chatIcon.style.display = 'none';
closeIcon.style.display = 'block';
fab.classList.add('is-active');
}
});
// Chat Logic
const API_URL = "https://joedown11-chatbot21.hf.space/api/chat";
const BASE_URL = API_URL.replace("/api/chat", "");
let chatHistory = [];
let _pollInterval = null;
let _lastPollId = 0;
let _wasBlocked = false;
let _wasQueued = false;
let _humanTypingEl = null; // Persistent 'agent typing' indicator during HUMAN_TAKEOVER
// Queue Timer State
let _queueEnteredTime = 0;
let _reminderUses = 3;
let _lastReminderTime = 0;
let _queueInterval = null;
function updateUIState(status, blocked) {
if (blocked) {
inputField.disabled = true;
inputField.placeholder = "Session blocked.";
sendButton.disabled = true;
if (!_wasBlocked) {
appendMessage('bot', "Your session has been restricted by the administrator. You cannot send further messages.");
_wasBlocked = true;
}
return;
}
if (status === 'QUEUED') {
inputField.disabled = true;
inputField.placeholder = "Waiting for human agent...";
sendButton.disabled = true;
if (!_wasQueued) {
_wasQueued = true;
_queueEnteredTime = Date.now();
_reminderUses = 3;
_lastReminderTime = 0;
startQueueTimer();
}
document.getElementById('queue-actions-area').style.display = 'flex';
} else if (status === 'ENDED') {
inputField.disabled = true;
inputField.placeholder = "Chat ended.";
sendButton.disabled = true;
document.getElementById('queue-actions-area').style.display = 'none';
stopQueueTimer();
_wasQueued = false;
if (!document.getElementById('btn-new-chat')) {
const newChatBtn = document.createElement('button');
newChatBtn.id = 'btn-new-chat';
newChatBtn.innerHTML = 'Start New Chat';
newChatBtn.style.cssText = 'width:calc(100% - 32px); margin:0 16px 16px 16px; padding:12px; background:#10b981; color:white; border:none; border-radius:8px; font-weight:600; cursor:pointer; font-family:inherit;';
newChatBtn.onclick = () => { ratingOverlay.classList.add('show'); };
document.getElementById('martech-chat-box').appendChild(newChatBtn);
}
} else {
// AI or HUMAN_TAKEOVER
inputField.disabled = false;
inputField.placeholder = "Type your question here...";
sendButton.disabled = false;
_wasQueued = false; // Reset queue state if taken over
document.getElementById('queue-actions-area').style.display = 'none';
stopQueueTimer();
// Remove 'Start New Chat' button if it exists
const btnNewChat = document.getElementById('btn-new-chat');
if (btnNewChat) btnNewChat.remove();
document.getElementById('chat-input-area').style.display = 'flex';
}
}
function startQueueTimer() {
if (_queueInterval) clearInterval(_queueInterval);
_queueInterval = setInterval(checkQueueTimer, 1000);
checkQueueTimer();
}
function stopQueueTimer() {
if (_queueInterval) clearInterval(_queueInterval);
}
function checkQueueTimer() {
const now = Date.now();
const btnRemind = document.getElementById('btn-remind');
const remindText = document.getElementById('remind-text');
if (_reminderUses <= 0) {
btnRemind.disabled = true;
remindText.textContent = "No reminders left";
return;
}
// Needs 3 minutes (180000 ms) from enter time OR from last reminder
const timeSinceEntered = now - _queueEnteredTime;
const timeSinceLastReminder = now - _lastReminderTime;
if (timeSinceEntered >= 180000 && (_lastReminderTime === 0 || timeSinceLastReminder >= 180000)) {
btnRemind.disabled = false;
remindText.textContent = `Remind Admin (${_reminderUses} left)`;
} else {
btnRemind.disabled = true;
// Calculate remaining time
const targetTime = _lastReminderTime > 0 ? _lastReminderTime + 180000 : _queueEnteredTime + 180000;
const remainingSec = Math.ceil((targetTime - now) / 1000);
const min = Math.floor(remainingSec / 60);
const sec = remainingSec % 60;
remindText.textContent = `Wait ${min}:${sec.toString().padStart(2, '0')}`;
}
}
// ── Session ID: generate once, persist in localStorage ──
let sessionId = localStorage.getItem('martech_session_id');
if (!sessionId) {
// Use a temporary name/id until we get the real one
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
localStorage.setItem('martech_session_id', sessionId);
}
// Bug fix: if existing session is still a generic temp ID, mark it for re-resolution
const _isGenericId = (id) => id && id.startsWith('user-') && /^user-\d+-[a-z0-9]+$/.test(id);
function formatMessage(text) {
if (!text) return "";
// 1. Convert Markdown Bold to HTML Bold (safety)
let formatted = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
// 2. Remove the '*' at the start of lines and use a bullet
formatted = formatted.replace(/^\* /gm, '• ');
// 3. Newlines to BR
return formatted.replace(/\n/g, '<br>');
}
function appendMessage(role, content) {
const div = document.createElement('div');
div.className = `chat-message ${role}`;
div.innerHTML = formatMessage(content);
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return div;
}
// ── Shared helper: resolve user name from all 3 options ─────────────────
async function _resolveUserName() {
// ── OPTION 0: Window injection by HR portal (fastest, zero cost) ──
// The HR portal page sets window.HR_USER_NAME = "John Doe" before this script loads
if (window.HR_USER_NAME && typeof window.HR_USER_NAME === 'string' && window.HR_USER_NAME.trim().length > 1) {
return window.HR_USER_NAME.trim();
}
// ── OPTION 1: Scan localStorage / sessionStorage for profile objects ──
try {
const storageKeys = ['userInfo', 'user', 'profile', 'auth', 'currentUser', 'account', 'userData', 'loginUser'];
for (const key of storageKeys) {
const stored = localStorage.getItem(key) || sessionStorage.getItem(key);
if (stored) {
try {
const parsed = JSON.parse(stored);
// Covers: {name}, {fullName}, {user:{name}}, {display_name}, {personalDetails:{fullName}}
const found = parsed.name
|| parsed.fullName
|| (parsed.user && parsed.user.name)
|| parsed.display_name
|| (parsed.personalDetails && parsed.personalDetails.fullName);
if (found && found.trim().length > 1) return found.trim();
} catch (err) {
// Plain string value (e.g. name stored directly)
if (typeof stored === 'string' && stored.length < 80 && !stored.includes('{')) {
return stored.trim();
}
}
}
}
} catch (e) { }
// ── OPTION 2: Call the HR profile API directly ──
// API confirmed to return: { name: "Ghulam Baqir", ... }
try {
const tokenKeys = ['token', 'accessToken', 'access_token', 'authToken', 'auth_token', 'jwt', 'jwtToken'];
let authToken = null;
for (const tk of tokenKeys) {
const tv = localStorage.getItem(tk) || sessionStorage.getItem(tk);
if (tv && tv.length > 20) { authToken = tv.replace(/^Bearer\s+/i, ''); break; }
}
const headers = { 'Content-Type': 'application/json' };
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
const pResp = await fetch("https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile", {
headers: headers,
credentials: 'include' // also sends cookies (session-based auth)
});
if (pResp.ok) {
const pData = await pResp.json();
// API top-level 'name' field is confirmed; personalDetails.fullName is fallback
const found = pData.name || (pData.personalDetails && pData.personalDetails.fullName);
if (found && found.trim().length > 1) return found.trim();
}
} catch (e) { }
return null; // all options exhausted
}
// ── User Integration & History ──────────────────────────────────────────
async function initUserSession() {
// Bug fix: if this is a returning visit but the stored ID is still a generic
// temp ID (resolution failed last time), retry resolution now
const needsResolution = _isGenericId(sessionId);
let userName = await _resolveUserName();
// Multi-user logic: if name changed since last visit, start a fresh session
const lastKnownName = localStorage.getItem('martech_last_user_name');
if (userName && lastKnownName && userName !== lastKnownName) {
localStorage.removeItem('martech_session_id');
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
localStorage.setItem('martech_session_id', sessionId);
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
}
if (userName) {
localStorage.setItem('martech_last_user_name', userName);
}
// ── PRE-CREATE SESSION AT BACKEND ON WIDGET LOAD ────────────────────
// This creates the session with the user's name BEFORE the user even
// opens the chat window, so it is immediately visible in the admin panel.
try {
const initData = await fetch(BASE_URL + "/api/session/init", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, user_name: userName || null })
}).then(r => r.json());
// Backend may return a canonical named session_id (e.g. "Ali_Hassan")
if (initData.session_id && initData.session_id !== sessionId) {
sessionId = initData.session_id;
localStorage.setItem('martech_session_id', sessionId);
}
} catch (e) {
// Non-fatal: session will still be created on first message if this fails
console.warn('Martech: background session pre-creation failed', e);
}
try {
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
if (hResp.ok) {
const hData = await hResp.json();
if (hData.status === 'HUMAN_TAKEOVER') {
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
} else if (hData.status === 'ENDED') {
document.getElementById('chat-status-dot').style.background = '#94a3b8';
document.getElementById('chat-header-title').textContent = 'Chat Ended';
} else {
document.getElementById('chat-status-dot').style.background = '#10b981';
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
}
updateUIState(hData.status, hData.blocked);
if (hData.last_id) {
_lastPollId = hData.last_id;
}
if (hData.history && hData.history.length > 0) {
messagesContainer.innerHTML = '';
chatHistory = hData.history;
chatHistory.forEach(m => {
appendMessage(m.role === 'assistant' ? 'bot' : 'user', m.content);
});
}
}
} catch (e) { }
// If name still not found, set up continuous lightweight background resolution
// so the session gets named as soon as auth is available
if (!userName) {
let _isResolving = false;
const _attemptSync = async () => {
// Stop checking if we already got a real name, or if a check is currently running
if (!_isGenericId(sessionId) || _isResolving) return;
_isResolving = true;
const resolved = await _resolveUserName();
if (resolved) {
localStorage.setItem('martech_last_user_name', resolved);
try {
// Re-init with the now-known name to promote temp → named session
const rData = await fetch(BASE_URL + "/api/session/init", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, user_name: resolved })
}).then(r => r.json());
if (rData.session_id && rData.session_id !== sessionId) {
sessionId = rData.session_id;
localStorage.setItem('martech_session_id', sessionId);
}
} catch (e) { }
}
_isResolving = false;
};
// Event 1: Instantly trigger when the main app writes a token to storage
window.addEventListener('storage', _attemptSync);
// Event 2: Instantly trigger when user refocuses the tab (e.g. after auth redirect)
window.addEventListener('focus', _attemptSync);
// Fallback: Check every 15 seconds (virtually zero cost once session is named)
setInterval(_attemptSync, 15000);
}
// Setup polling for admin messages
if (_pollInterval) clearInterval(_pollInterval);
_pollInterval = setInterval(pollMessages, 3000);
}
async function pollMessages() {
if (!sessionId) return;
try {
const r = await fetch(BASE_URL + "/api/chat/poll", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, last_id: _lastPollId })
});
if (r.ok) {
const data = await r.json();
_lastPollId = data.last_id;
if (data.status === 'HUMAN_TAKEOVER') {
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
} else if (data.status === 'ENDED') {
document.getElementById('chat-status-dot').style.background = '#94a3b8';
document.getElementById('chat-header-title').textContent = 'Chat Ended';
} else {
document.getElementById('chat-status-dot').style.background = '#10b981';
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
}
updateUIState(data.status, data.blocked);
if (data.new_messages && data.new_messages.length > 0) {
// Remove the 'Human Agent is typing...' indicator before showing the real reply
if (_humanTypingEl) {
_humanTypingEl.remove();
_humanTypingEl = null;
}
data.new_messages.forEach(m => {
appendMessage('bot', m.content);
chatHistory.push({ role: "assistant", content: m.content });
});
}
}
} catch (e) { }
}
initUserSession();
function showTypingIndicator() {
const div = document.createElement('div');
div.className = 'chat-message bot';
div.style.padding = '8px 16px';
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return div;
}
// Typo Configuration for Human-like Typing
const NEARBY_KEYS = {
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
'z': 'xas',
};
const TYPO_CHANCE = 0.07;
function getNearbyChar(ch) {
const lower = ch.toLowerCase();
if (NEARBY_KEYS[lower]) {
const options = NEARBY_KEYS[lower];
const replacement = options.charAt(Math.floor(Math.random() * options.length));
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
}
const abc = "abcdefghijklmnopqrstuvwxyz";
return abc.charAt(Math.floor(Math.random() * abc.length));
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function typeMessageWithTypos(element, text) {
const fullHtml = formatMessage(text);
// Simple HTML-aware typing: split by tags
const tokens = fullHtml.split(/(<[^>]*>)/);
let currentHtml = "";
for (const token of tokens) {
if (token.startsWith("<")) {
// Instantly add tags
currentHtml += token;
element.innerHTML = currentHtml;
} else {
// Type text content char by char
for (let char of token) {
currentHtml += char;
element.innerHTML = currentHtml;
await sleep(Math.random() * 15 + 5);
}
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
async function handleSend() {
const text = inputField.value.trim();
if (!text) return;
// Remove any stale 'Human Agent is typing...' indicator before sending a new message
if (_humanTypingEl) {
_humanTypingEl.remove();
_humanTypingEl = null;
}
// UI state lock
inputField.value = '';
inputField.disabled = true;
sendButton.disabled = true;
appendMessage('user', text);
const typingEl = showTypingIndicator();
try {
// Call the FastAPI Backend directly
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
history: chatHistory,
session_id: sessionId
})
});
if (!response.ok) {
if (response.status === 429) throw new Error("Rate limit exceeded.");
throw new Error("Server error.");
}
const data = await response.json();
const reply = data.reply;
if (reply && reply.trim()) {
// Normal AI reply — remove temporary indicator, show message
typingEl.remove();
const botEl = appendMessage('bot', '');
chatHistory.push({ role: "user", content: text });
chatHistory.push({ role: "assistant", content: reply });
await typeMessageWithTypos(botEl, reply);
} else {
// HUMAN_TAKEOVER mode: backend returned empty reply.
// Repurpose the typing indicator as 'Human Agent is typing...' and keep it visible.
typingEl.innerHTML = `
<div style="display:flex; align-items:center; justify-content:flex-start; gap:6px;">
<span style="font-size:13px; color:#8b5cf6; font-weight:600; line-height:1; margin-top:2px;">typing</span>
<div class="typing-indicator" style="height:18px; padding:0; margin:0;"><span></span><span></span><span></span></div>
</div>`;
typingEl.style.padding = '8px 16px';
typingEl.style.lineHeight = '0.2';
typingEl.style.borderColor = 'rgba(139,92,246,0.25)';
typingEl.style.background = 'rgba(139,92,246,0.05)';
_humanTypingEl = typingEl; // Keep reference so pollMessages can remove it
chatHistory.push({ role: "user", content: text });
}
} catch (error) {
console.error(error);
typingEl.remove();
if (_humanTypingEl === typingEl) _humanTypingEl = null;
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
} finally {
inputField.disabled = false;
sendButton.disabled = false;
inputField.focus();
}
}
// Event Listeners
sendButton.addEventListener('click', handleSend);
inputField.addEventListener('keypress', function (e) {
if (e.key === 'Enter') handleSend();
});
document.getElementById('btn-remind').addEventListener('click', async () => {
if (_reminderUses <= 0) return;
try {
await fetch(BASE_URL + "/api/chat/remind", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
_reminderUses--;
_lastReminderTime = Date.now();
checkQueueTimer();
const remindText = document.getElementById('remind-text');
remindText.textContent = "Reminder Sent!";
setTimeout(() => checkQueueTimer(), 2000);
} catch (e) {}
});
document.getElementById('btn-exit-queue').addEventListener('click', async () => {
try {
document.getElementById('queue-actions-area').style.display = 'none';
appendMessage('bot', "You have left the queue and are back to chatting with the AI.");
await fetch(BASE_URL + "/api/chat/exit_queue", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
// Force a poll to sync state
pollMessages();
} catch (e) {}
});
// Rating Logic
const ratingOverlay = document.getElementById('rating-overlay');
const stars = document.querySelectorAll('#rating-stars span');
let _selectedRating = 0;
stars.forEach(s => {
s.addEventListener('click', () => {
_selectedRating = parseInt(s.dataset.val);
stars.forEach(st => {
st.classList.toggle('active', parseInt(st.dataset.val) <= _selectedRating);
});
});
});
document.getElementById('btn-submit-rating').addEventListener('click', async () => {
if (_selectedRating > 0) {
try {
await fetch(BASE_URL + "/api/chat/rate", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, rating: _selectedRating })
});
} catch (e) { }
}
// Reset current chat thread instead of deleting session
try {
await fetch(BASE_URL + "/api/chat/reset", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
} catch (e) { }
chatHistory = [];
_lastPollId = 0;
_wasBlocked = false;
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
ratingOverlay.classList.remove('show');
document.getElementById('chat-status-dot').style.background = '#10b981';
const titleEl = document.getElementById('chat-header-title');
titleEl.textContent = 'Martechsol Assistant';
document.getElementById('chat-input-area').style.display = 'flex';
inputField.disabled = false;
sendButton.disabled = false;
inputField.placeholder = "Type your question here...";
const newChatBtn = document.getElementById('btn-new-chat');
if (newChatBtn) newChatBtn.remove();
fab.click(); // Close chat UI
// Prep for next open
setTimeout(() => {
initUserSession();
}, 500);
});
})();
</script>
</body>
</html>