PageSourceSearch

https://sadv.co.za/wp-content/plugins/sadv-chatbot/assets/chatbot.js

js sadv.co.za collected 2026-10-01 15:53:54 UTC 17,598 bytes, 431 lines download raw bytes

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    }, '&#8722;'); // 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    }, '&#10005;'); // 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.