1/** 2 * SADV Chatbot â Frontend JS v1.2.0 3 * All configuration comes from sadvChatbotConfig (wp_localize_script). 4 * Zero hardcoded values. 5 */ 6(function () { 7 'use strict'; 8 9 // Safety check â config must exist 10 if (typeof sadvChatbotConfig === 'undefined') { 11 console.error('SADV Chatbot: sadvChatbotConfig not found.'); 12 return; 13 } 14 15 var cfg = sadvChatbotConfig; 16 17 /* ------------------------------------------------------------------ 18 * APPLY CSS CUSTOM PROPERTIES FROM CONFIG 19 * ------------------------------------------------------------------ */ 20 var root = document.documentElement; 21 root.style.setProperty('--sadv-primary', cfg.primaryColor); 22 root.style.setProperty('--sadv-header-bg', cfg.headerBgColor); 23 root.style.setProperty('--sadv-highlight', cfg.highlightColor); 24 root.style.setProperty('--sadv-user-bubble-bg', cfg.userBubbleBg); 25 root.style.setProperty('--sadv-user-bubble-txt', cfg.userBubbleText); 26 root.style.setProperty('--sadv-bot-bubble-bg', cfg.botBubbleBg); 27 root.style.setProperty('--sadv-bot-bubble-bdr', cfg.botBubbleBorder); 28 root.style.setProperty('--sadv-opt-btn-bdr', cfg.optionBtnBorder); 29 root.style.setProperty('--sadv-opt-btn-txt', cfg.optionBtnText); 30 31 /* ------------------------------------------------------------------ 32 * DOM HELPERS 33 * ------------------------------------------------------------------ */ 34 function el(tag, attrs, innerHTML) { 35 var e = document.createElement(tag); 36 attrs = attrs || {}; 37 for (var k in attrs) { 38 if (Object.prototype.hasOwnProperty.call(attrs, k)) { 39 e.setAttribute(k, attrs[k]); 40 } 41 } 42 if (innerHTML !== undefined) e.innerHTML = innerHTML; 43 return e; 44 } 45 46 /* ------------------------------------------------------------------ 47 * BUILD WIDGET DOM 48 * ------------------------------------------------------------------ */ 49 var root_el = document.getElementById('sadv-chatbot-root'); 50 if (!root_el) return; 51 52 // ââ Open button ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 53 var openBtn = el('button', { 54 id: 'sadv-chatbot-open-btn', 55 'aria-label': 'Open Chatbot' 56 }); 57 var openImg = el('img', { 58 src: cfg.openButtonImage, 59 alt: cfg.botName, 60 style: 'width:' + cfg.openButtonWidth + 'px;height:auto;display:block;' 61 }); 62 openBtn.appendChild(openImg); 63 openBtn.style.bottom = cfg.openBtnBottom + 'px'; 64 openBtn.style.right = cfg.openBtnRight + 'px'; 65 document.body.appendChild(openBtn); 66 67 // ââ Speech bubble ââââââââââââââââââââââââââââââââââââââââââââââââââââ 68 var speechBubble = null; 69 if (cfg.showSpeechBubble && cfg.speechBubbleText) { 70 speechBubble = el('div', { id: 'sadv-speech-bubble' }, 71 cfg.speechBubbleText.replace(/\n/g, '<br>')); 72 document.body.appendChild(speechBubble); 73 positionSpeechBubble(); 74 window.addEventListener('resize', positionSpeechBubble); 75 } 76 77 function positionSpeechBubble() { 78 if (!speechBubble) return; 79 var btnRect = openBtn.getBoundingClientRect(); 80 speechBubble.style.bottom = (window.innerHeight - btnRect.top + 10) + 'px'; 81 speechBubble.style.right = (window.innerWidth - btnRect.right + 10) + 'px'; 82 } 83 84 // ââ Chat container âââââââââââââââââââââââââââââââââââââââââââââââââââ 85 var container = el('div', { id: 'sadv-chatbot-container' }); 86 container.style.width = cfg.widgetWidth + 'px'; 87 container.style.bottom = cfg.widgetBottom + 'px'; 88 container.style.right = cfg.widgetRight + 'px'; 89 document.body.appendChild(container); 90 91 // ââ Header âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 92 // Header uses flexbox: [avatar?] [title â grows] [â btn] [X btn] 93 var header = el('div', { id: 'sadv-bot-header' }); 94 container.appendChild(header); 95 96 // ââ Avatar (INSIDE header, anchored bottom:0 so it extends UPWARD only) 97 var avatar = null; 98 if (cfg.avatarShow) { 99 avatar = el('img', { 100 id: 'sadv-chatbot-avatar', 101 src: cfg.headerAvatarImage, 102 alt: cfg.botName + ' avatar' 103 }); 104 avatar.style.width = parseInt(cfg.avatarWidth, 10) + 'px'; 105 avatar.style.height = 'auto'; 106 avatar.style.position = 'absolute'; 107 // avatarVOffset: how many px above the header bottom the avatar sits. 108 // 0 = feet touch header bottom. 60 = avatar raised 60px, body floats above header. 109 avatar.style.bottom = parseInt(cfg.avatarVOffset, 10) + 'px'; 110 avatar.style.pointerEvents = 'none'; 111 avatar.style.zIndex = '2'; 112 113 if (cfg.avatarHAlign === 'right') { 114 avatar.style.right = cfg.avatarHOffset + 'px'; 115 avatar.style.left = 'auto'; 116 } else if (cfg.avatarHAlign === 'center') { 117 avatar.style.left = '50%'; 118 avatar.style.right = 'auto'; 119 avatar.style.transform = 'translateX(-50%)'; 120 } else { 121 // left (default) 122 avatar.style.left = cfg.avatarHOffset + 'px'; 123 avatar.style.right = 'auto'; 124 } 125 126 header.appendChild(avatar); 127 } 128 129 // ââ Header title âââââââââââââââââââââââââââââââââââââââââââââââââââââ 130 var titleEl = el('h6', { id: 'sadv-chatbot-title' }, cfg.botTitle); 131 132 // Apply title styling from settings 133 titleEl.style.fontSize = parseInt(cfg.titleFontSize, 10) + 'px'; 134 titleEl.style.fontWeight = cfg.titleFontWeight; 135 titleEl.style.textAlign = cfg.titleAlign; 136 titleEl.style.color = cfg.titleColor; 137 138 // Give the title padding so it clears the avatar â parseInt ensures numbers 139 if (cfg.avatarShow) { 140 var avatarW = parseInt(cfg.avatarWidth, 10); 141 var avatarOff = parseInt(cfg.avatarHOffset, 10); 142 var clearance = (avatarW + avatarOff + 10) + 'px'; 143 if (cfg.avatarHAlign === 'left') { 144 titleEl.style.paddingLeft = clearance; 145 titleEl.style.paddingRight = '0'; 146 } else if (cfg.avatarHAlign === 'right') { 147 titleEl.style.paddingRight = clearance; 148 titleEl.style.paddingLeft = '0'; 149 } else { 150 // center â avatar is above, no horizontal padding needed 151 titleEl.style.paddingLeft = '0'; 152 titleEl.style.paddingRight = '0'; 153 } 154 } 155 header.appendChild(titleEl); 156 157 // ââ Header buttons âââââââââââââââââââââââââââââââââââââââââââââââââââ 158 var headerBtns = el('div', { id: 'sadv-header-btns' }); 159 160 var minimiseBtn = el('button', { 161 id: 'sadv-chatbot-minimise',
162 'aria-label': 'Minimise chat', 163 title: 'Minimise' 164 }, '−'); // proper minus sign 165 headerBtns.appendChild(minimiseBtn); 166 167 var endBtn = el('button', { 168 id: 'sadv-chatbot-end', 169 'aria-label': 'End chat', 170 title: 'End chat' 171 }, '✕'); // proper à 172 headerBtns.appendChild(endBtn); 173 174 header.appendChild(headerBtns); 175 176 // ââ Chat area ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 177 var chatArea = el('div', { id: 'sadv-chat-area', role: 'log', 'aria-live': 'polite' }); 178 container.appendChild(chatArea); 179 180 // ââ Input area âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 181 var inputArea = el('div', { id: 'sadv-input-area' }); 182 var userInput = el('input', { 183 id: 'sadv-chatbot-input', 184 type: 'text', 185 placeholder: cfg.inputPlaceholder, 186 'aria-label': 'Type your message' 187 }); 188 var sendBtn = el('button', { id: 'sadv-chatbot-send' }, cfg.sendButtonText); 189 inputArea.appendChild(userInput); 190 inputArea.appendChild(sendBtn); 191 container.appendChild(inputArea); 192 193 /* ------------------------------------------------------------------ 194 * STATE 195 * ------------------------------------------------------------------ */ 196 var interactionId = null; 197 var token = null; 198 var socket = null; 199 var chatInitialized = false; 200 var retryCount = 0; 201 var maxRetries = cfg.maxRetries; 202 var socketIntentionallyClosed = false; 203 204 /* ------------------------------------------------------------------ 205 * SHOW / HIDE 206 * ------------------------------------------------------------------ */ 207 function openChat() { 208 container.style.display = 'flex'; 209 openBtn.style.display = 'none'; 210 if (speechBubble) speechBubble.style.display = 'none'; 211 ensureChatInitialized(cfg.initialMessage); 212 } 213 214 function closeChat(fullEnd) { 215 container.style.display = 'none'; 216 openBtn.style.display = 'block'; 217 if (speechBubble) { 218 speechBubble.style.display = 'block'; 219 positionSpeechBubble(); 220 } 221 if (fullEnd) { 222 chatInitialized = false; 223 interactionId = null; 224 token = null; 225 socketIntentionallyClosed = true; 226 if (socket) { try { socket.close(); } catch (e) {} } 227 addMessage('This conversation has ended. Click the button to start a new chat.', 'bot'); 228 } 229 } 230 231 openBtn.addEventListener('click', openChat); 232 minimiseBtn.addEventListener('click', function () { closeChat(false); }); 233 endBtn.addEventListener('click', function () { closeChat(true); }); 234 235 if (cfg.autoOpen) { openChat(); } 236 237 /* ------------------------------------------------------------------ 238 * CHAT INITIALISATION 239 * ------------------------------------------------------------------ */ 240 function ensureChatInitialized(msg) { 241 if (!chatInitialized) { initializeChat(msg); } 242 } 243 244 function initializeChat(initialMessage) { 245 socketIntentionallyClosed = false; 246 fetch(cfg.apiBaseUrl, { 247 method: 'POST', 248 headers: { 'Content-Type': 'application/json' }, 249 body: JSON.stringify({ 250 org_id: cfg.orgId, 251 eid: cfg.eid, 252 title: 'Webchat', 253 body: initialMessage 254 }) 255 }) 256 .then(function (res) { 257 if (!res.ok) throw new Error('Failed to initialise chat'); 258 return res.json(); 259 }) 260 .then(function (data) { 261 interactionId = data.interaction_id; 262 token = data.token; 263 chatInitialized = true; 264 connectWebSocket(data.events_url); 265 }) 266 .catch(function (err) { 267 console.error('SADV Chatbot init error:', err); 268 addMessage('Failed to connect. Please try again.', 'bot'); 269 }); 270 } 271 272 /* ------------------------------------------------------------------ 273 * WEBSOCKET 274 * ------------------------------------------------------------------ */ 275 function connectWebSocket(eventsUrl) { 276 function attempt() { 277 if (retryCount >= maxRetries) {
278 addMessage('Unable to reconnect. Please refresh the page.', 'bot'); 279 return; 280 } 281 socket = new WebSocket(cfg.wsBaseUrl + eventsUrl); 282 283 socket.onopen = function () { 284 console.log('SADV Chatbot: WebSocket connected.'); 285 retryCount = 0; 286 }; 287 socket.onmessage = function (event) { 288 var data; 289 try { data = JSON.parse(event.data); } catch (e) { return; } 290 handleWsMessage(data); 291 }; 292 socket.onerror = function () { 293 retryCount++; 294 if (retryCount < maxRetries) { 295 setTimeout(attempt, Math.pow(2, retryCount) * 1000); 296 } 297 }; 298 socket.onclose = function () { 299 if (socketIntentionallyClosed) return; 300 retryCount++; 301 setTimeout(attempt, Math.pow(2, retryCount) * 1000); 302 }; 303 } 304 attempt(); 305 } 306 307 function handleWsMessage(data) { 308 if (data.type === 'websocket:register') { return; } 309 if (Array.isArray(data)) { data.forEach(processMessage); } 310 else { processMessage(data); } 311 } 312 313 function processMessage(msg) { 314 if (msg.body && msg.agent) { 315 addMessage(msg.body, 'bot'); 316 } 317 if (msg.additional_data && msg.additional_data.options) { 318 displayOptions(msg.additional_data.options); 319 } 320 } 321 322 /* ------------------------------------------------------------------ 323 * SEND MESSAGE 324 * ------------------------------------------------------------------ */ 325 function sendMessage(message) { 326 if (!message) return; 327 ensureChatInitialized(message); 328 329 if (!interactionId || !token) { 330 addMessage('Chat not ready. Please wait a moment and try again.', 'bot'); 331 return; 332 } 333 334 if (socket && socket.readyState === WebSocket.OPEN) { 335 socket.send(JSON.stringify({ 336 msg: 'QCONTACT_TYPING', name: 'me', type: 'websocket:broadcast' 337 })); 338 } 339 340 fetch(cfg.apiBaseUrl + '/' + interactionId + '/' + token + '/message', { 341 method: 'POST', 342 headers: { 'Content-Type': 'application/json' }, 343 body: JSON.stringify({ body: message }) 344 }) 345 .then(function (res) { 346 if (!res.ok) throw new Error('Send failed'); 347 return res.json(); 348 }) 349 .then(function (data) { 350 if (data.error && data.error === 'This conversation has already ended') { 351 addMessage('This conversation has already ended. Please start a new chat.', 'bot'); 352 } 353 }) 354 .catch(function () { 355 addMessage('Error sending message. Please try again.', 'bot'); 356 }); 357 } 358 359 /* ------------------------------------------------------------------ 360 * UI â ADD MESSAGE 361 * ------------------------------------------------------------------ */ 362 function addMessage(text, sender) { 363 var existing = document.getElementById('sadv-chat-options'); 364 if (existing) existing.remove(); 365 366 var wrap = el('div', { class: 'sadv-msg-wrap' + (sender === 'user' ? ' sadv-user' : '') }); 367 368 var ts = el('span', { class: 'sadv-timestamp' }); 369 ts.textContent = new Date().toLocaleTimeString(); 370 wrap.appendChild(ts); 371 372 var bubble = el('p', { 373 class: 'sadv-bubble ' + (sender === 'user' ? 'sadv-bubble-user' : 'sadv-bubble-bot') 374 }); 375 376 if (sender === 'user') { 377 bubble.textContent = text; 378 } else { 379 var botNameEsc = cfg.botName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 380 var renderedText = text.replace( 381 new RegExp(botNameEsc, 'g'), 382 '<span class="sadv-bot-highlight">' + cfg.botName + '</span>' 383 ); 384 bubble.innerHTML = renderedText; 385 } 386 387 wrap.appendChild(bubble); 388 chatArea.appendChild(wrap); 389 chatArea.scrollTop = chatArea.scrollHeight; 390 } 391 392 /* ------------------------------------------------------------------ 393 * UI â DISPLAY OPTIONS 394 * ------------------------------------------------------------------ */ 395 function displayOptions(options) { 396 var existing = document.getElementById('sadv-chat-options'); 397 if (existing) existing.remove(); 398 399 var optWrap = el('div', { id: 'sadv-chat-options' }); 400 options.forEach(function (opt) { 401 var btn = el('button', {}, opt.display); 402 btn.addEventListener('click', function () { 403 addMessage(opt.display, 'user'); 404 sendMessage(opt.responses[0]); 405 optWrap.remove(); 406 }); 407 optWrap.appendChild(btn); 408 }); 409 chatArea.appendChild(optWrap); 410 chatArea.scrollTop = chatArea.scrollHeight; 411 } 412 413 /* ------------------------------------------------------------------ 414 * SEND BUTTON + ENTER KEY 415 * ------------------------------------------------------------------ */ 416 sendBtn.addEventListener('click', function () { 417 var msg = userInput.value.trim(); 418 if (msg) { 419 addMessage(msg, 'user'); 420 sendMessage(msg); 421 userInput.value = ''; 422 } else { 423 addMessage('Please enter your message.', 'bot'); 424 } 425 }); 426 427 userInput.addEventListener('keydown', function (e) { 428 if (e.key === 'Enter') { e.preventDefault(); sendBtn.click(); } 429 }); 430 431})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.