PageSourceSearch

https://licensemanager.at/wp-content/plugins/aichatbot-premium/assets/widget.js?ver=2.6.8

js licensemanager.at collected 2026-10-03 22:10:55 UTC 29,686 bytes, 780 lines download raw bytes

1(function (){
2    'use strict';
3
4    var container = document.getElementById('pkb-widget-container');
5    if (!container) return;
6
7    // Read config from data attributes (caching-plugin safe!)
8    var API_URL  = (container.getAttribute('data-api-url') || '').replace(/\/+$/, '') + '/ask';
9    var API_KEY  = container.getAttribute('data-api-key') || '';
10    var PRODUCT  = container.getAttribute('data-product') || 'mycred';
11    var COLOR    = container.getAttribute('data-color') || '#2271b1';
12    var BOT_NAME = container.getAttribute('data-title') || 'Support Bot';
13
14    if (!API_URL || !API_KEY) {
15        console.warn('PKB: missing API URL or key');
16        return;
17    }
18
19    var panel     = document.getElementById('pkb-panel');
20    var toggleBtn = document.getElementById('pkb-toggle');
21    var headerClose = document.getElementById('pkb-header-close');
22    var headerExpand = document.getElementById('pkb-header-expand');
23    var headerCollapse = document.getElementById('pkb-header-collapse');
24    var messagesEl = document.getElementById('pkb-messages');
25    var inputEl   = document.getElementById('pkb-input');
26    var sendBtn   = document.getElementById('pkb-send');
27
28    // "Start New Chat" button — created dynamically, shown when chat is resolved
29    var newChatBtn = document.createElement('button');
30    newChatBtn.id = 'pkb-new-chat-btn';
31    newChatBtn.textContent = 'Start New Chat';
32    newChatBtn.style.display = 'none';
33    (function() {
34        var inputArea = document.getElementById('pkb-input-area');
35        if (inputArea) inputArea.appendChild(newChatBtn);
36    })();
37
38    var SESSION_KEY = 'pkb_conv_' + PRODUCT;
39
40    // --- Live Chat state ---
41    var LIVE_CHAT_REST = (container.getAttribute('data-livechat-rest') || '').replace(/\/+$/, '');
42    var LIVE_CHAT_KEY = 'pkb_livechat_' + PRODUCT;
43    var liveChatChatId = null;
44    var liveChatLastMsgId = 0;
45    var liveChatPollTimer = null;
46    var liveChatStatus = null;
47    var chatResolved = false;
48    var RESOLVED_KEY = 'pkb_resolved_' + PRODUCT;
49
50    // Restore live chat session from sessionStorage (survives page refresh)
51    try {
52        var savedChat = sessionStorage.getItem(LIVE_CHAT_KEY);
53        if (savedChat) {
54            var parsed = JSON.parse(savedChat);
55            if (parsed.chat_id && parsed.status !== 'resolved') {
56                liveChatChatId = parseInt(parsed.chat_id, 10);
57                liveChatLastMsgId = parseInt(parsed.last_msg_id || 0, 10);
58                liveChatStatus = parsed.status || null;
59            } else {
60                sessionStorage.removeItem(LIVE_CHAT_KEY);
61            }
62        }
63    } catch(e) {}
64
65    // Restore resolved state (so resolved UI survives page refresh)
66    try {
67        if (sessionStorage.getItem(RESOLVED_KEY) === '1') chatResolved = true;
68    } catch(e) {}
69
70    var conversation = [];
71
72    // Read user info from sessionStorage (set by popup in functions.php)
73    // NOTE: read dynamically on every ask() — the popup may save them AFTER init.
74    function getUserInfo() {
75        try {
76            return {
77                name: sessionStorage.getItem('pkb_user_name') || '',
78                email: sessionStorage.getItem('pkb_user_email') || ''
79            };
80        } catch(e) { return { name: '', email: '' }; }
81    }
82    try {
83        var saved = sessionStorage.getItem(SESSION_KEY);
84        if (saved) conversation = JSON.parse(saved);
85    } catch(e) {}
86
87    // Expose addBotMessage globally so functions.php popup can call it
88    window.pkb_addBotMessage = function(text) {
89        addMessage(text, 'bot');
90        conversation.push({ role: 'assistant', content: text });
91        try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(conversation)); } catch(e) {}
92    };
93
94    // Apply brand color as CSS variable
95    container.style.setProperty('--pkb-color', COLOR);
96    var header = document.getElementById('pkb-header');
97    if (header) header.style.background = COLOR;
98    toggleBtn.style.background = COLOR;
99    sendBtn.style.background = COLOR;
100
101    // --- Avatar SVGs ---
102
103    var BOT_AVATAR = '<svg viewBox="0 0 24 24" fill="#555" width="18" height="18"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>';
104
105    var USER_AVATAR = '<svg viewBox="0 0 24 24" fill="white" width="18" height="18"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>';
106
107    // --- Status texts for cycling ---
108    // Reads from data-typing-statuses attribute (set in plugin settings).
109    // Falls back to built-in defaults if the attribute is empty or unset.
110    var STATUS_TEXTS = (function() {
111        var raw = container.getAttribute('data-typing-statuses') || '';
112        try {
113            var parsed = JSON.parse(raw);
114            if (Array.isArray(parsed) && parsed.length > 0) return parsed;
115        } catch(e) {}
116        return ['Reading docs', 'Analyzing', 'Processing', 'Searching KB', 'Formulating', 'Thinking', 'Searching', 'Looking', 'Hmm'];
117    })();
118    var statusInterval = null;
119
120    // --- UI Helpers ---
121
122    function scrollToBottom() {
123        setTimeout(function() { messagesEl.scrollTop = messagesEl.scrollHeight; }, 50);
124    }
125
126    function makeAvatar(role) {
127        var avatar = document.createElement('div');
128        avatar.className = 'pkb-avatar';
129        avatar.innerHTML = role === 'bot' ? BOT_AVATAR : USER_AVATAR;
130        return avatar;
131    }
132
133    function addMessage(text, role, senderName, timestamp) {
134        var div = document.createElement('div');
135        div.className = 'pkb-msg ' + (role === 'user' ? 'pkb-user' : 'pkb-bot');
136
137        // Bot messages get avatar on the left
138        if (role === 'bot') {
139            div.appendChild(makeAvatar('bot'));
140        }
141
142        var bubble = document.createElement('div');
143        bubble.className = 'pkb-msg-content';
144
145        // Sender name label (small, above the message)
146        // Bot: always show bot name; Agent: show agent name (live chat); User: show "You"
147        var nameToShow;
148        if (senderName) {
149            nameToShow = senderName;
150        } else if (role === 'bot') {
151            nameToShow = BOT_NAME;
152        } else {
153            nameToShow = 'You';
154        }
155        if (nameToShow) {
156            var nameLabel = document.createElement('div');
157            nameLabel.className = 'pkb-msg-sender-name';
158            nameLabel.textContent = nameToShow;
159            bubble.appendChild(nameLabel);
160        }
161
162        // Convert markdown-style formatting to HTML, escape raw HTML
163        var html = text
164            .replace(/&/g, '&amp;')
165            .replace(/</g, '&lt;')
166            .replace(/>/g, '&gt;')
167            // Bold: **text**
168            .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
169            // Italic: *text*
170            .replace(/\*([^*]+)\*/g, '<em>$1</em>')
171            // Inline code: `text`
172            .replace(/`([^`]+)`/g, '<code>$1</code>')
173            // Underline: __text__
174            .replace(/__([^_]+)__/g, '<u>$1</u>')
175            // Strikethrough: ~~text~~
176            .replace(/~~([^~]+)~~/g, '<s>$1</s>')
177            // URLs -> clickable links
178            .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
179
180        var body = document.createElement('div');
181        body.className = 'pkb-msg-body';
182        body.innerHTML = html.replace(/\n/g, '<br>');
183        bubble.appendChild(body);
184
185        // Timestamp footer below every message
186        var timeFooter = document.createElement('div');
187        timeFooter.className = 'pkb-msg-time';
188        if (timestamp) {
189            var d = new Date(timestamp);
190            timeFooter.textContent = isNaN(d.getTime()) ? timestamp : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
191        } else {
192            timeFooter.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
193        }
194        bubble.appendChild(timeFooter);
195
196        div.appendChild(bubble);
197
198        // User messages get avatar on the right
199        if (role === 'user') {
200            div.appendChild(makeAvatar('user'));
201        }
202
203        messagesEl.appendChild(div);
204        scrollToBottom();
205    }
206
207    // --- System notification (agent joined, etc.) ---
208
209    function addSystemMessage(text) {
210        var div = document.createElement('div');
211        div.className = 'pkb-msg pkb-system';
212        var span = document.createElement('span');
213        span.className = 'pkb-system-msg';
214        span.textContent = text;
215        div.appendChild(span);
216        messagesEl.appendChild(div);
217        scrollToBottom();
218    }
219
220    // --- Animated status text cycling ---
221
222    function buildStatusHTML() {
223        var html = '<div class="pkb-typing-status">';
224        for (var i = 0; i < STATUS_TEXTS.length; i++) {
225            html += '<span>' + STATUS_TEXTS[i] + '…</span>';
226        }
227        html += '</div>';
228        return html;
229    }
230
231    function showTyping() {
232        var div = document.createElement('div');
233        div.className = 'pkb-msg pkb-bot';
234        div.id = 'pkb-typing-indicator';
235        div.appendChild(makeAvatar('bot'));
236        div.innerHTML += '<div class="pkb-msg-content pkb-typing"><div class="pkb-typing-dots"><span></span><span></span><span></span></div></div>';
237
238        // Status text OUTSIDE the bubble, after the message row
239        var statusRow = document.createElement('div');
240        statusRow.className = 'pkb-typing-status-row';
241        statusRow.innerHTML = buildStatusHTML();
242        div.appendChild(statusRow);
243
244        messagesEl.appendChild(div);
245        scrollToBottom();
246
247        // Start cycling status texts
248        var statusEl = div.querySelector('.pkb-typing-status');
249        if (statusEl) {
250            var spans = statusEl.querySelectorAll('span');
251            var idx = Math.floor(Math.random() * spans.length);
252            spans[idx].classList.add('active');
253            statusInterval = setInterval(function() {
254                var next;
255                do {
256                    next = Math.floor(Math.random() * spans.length);
257                } while (next === idx && spans.length > 1);
258                spans[idx].classList.remove('active');
259                idx = next;
260                spans[idx].classList.add('active');
261            }, 1800);
262        }
263    }
264
265    function hideTyping() {
266        if (statusInterval) {
267            clearInterval(statusInterval);
268            statusInterval = null;
269        }
270        var el = document.getElementById('pkb-typing-indicator');
271        if (el) el.remove();
272    }
273
274    function setLoading(loading) {
275        sendBtn.disabled = loading;
276        inputEl.disabled = loading;
277    }
278
279    // --- Live Chat: poll for agent/system messages ---
280
281    function startLiveChatPolling(chatId) {
282        if (liveChatPollTimer) return; // already polling
283        liveChatChatId = chatId;
284        liveChatLastMsgId = 0;
285        liveChatStatus = null;
286        // Persist so page refresh doesn't lose the session
287        try { sessionStorage.setItem(LIVE_CHAT_KEY, JSON.stringify({ chat_id: chatId, last_msg_id: 0, status: null })); } catch(e) {}
288        liveChatPoll();
289        liveChatPollTimer = setInterval(liveChatPoll, 5000);
290    }
291
292    function stopLiveChatPolling() {
293        if (liveChatPollTimer) {
294            clearInterval(liveChatPollTimer);
295            liveChatPollTimer = null;
296        }
297        liveChatChatId = null;
298        liveChatLastMsgId = 0;
299        liveChatStatus = null;
300        try { sessionStorage.removeItem(LIVE_CHAT_KEY); } catch(e) {}
301    }
302
303    function showResolvedUI() {
304        chatResolved = true;
305        try { sessionStorage.setItem(RESOLVED_KEY, '1'); } catch(e) {}
306        inputEl.style.display = 'none';
307        sendBtn.style.display = 'none';
308        newChatBtn.style.display = '';
309    }
310
311    function hideResolvedUI() {
312        chatResolved = false;
313        try { sessionStorage.removeItem(RESOLVED_KEY); } catch(e) {}
314        inputEl.style.display = '';
315        sendBtn.style.display = '';
316        newChatBtn.style.display = 'none';
317        inputEl.focus();
318    }
319
320    function startNewChat() {
321        // Kill existing live chat session
322        stopLiveChatPolling();
323        liveChatChatId = null;
324        liveChatLastMsgId = 0;
325        liveChatStatus = null;
326        chatResolved = false;
327
328        // Clear bot conversation history
329        conversation = [];
330        try {
331            sessionStorage.removeItem(SESSION_KEY);
332            sessionStorage.removeItem(LIVE_CHAT_KEY);
333            sessionStorage.removeItem(RESOLVED_KEY);
334        } catch(e) {}
335
336        // Clear all messages from UI
337        messagesEl.innerHTML = '';
338
339        // Show fresh welcome message
340        var welcomeMsgText = container.getAttribute('data-welcome') || '👋 Hello! How can I help you today?';
341        addMessage(welcomeMsgText, 'bot');
342        conversation.push({ role: 'assistant', content: welcomeMsgText });
343        try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(conversation)); } catch(e) {}
344
345        // Restore normal input area
346        hideResolvedUI();
347        inputEl.focus();
348        scrollToBottom();
349    }
350
351    async function liveChatPoll() {
352        if (!liveChatChatId || !LIVE_CHAT_REST) return;
353        try {
354            var url = LIVE_CHAT_REST + '/pkb/v1/live-chat/' + liveChatChatId + '/messages';
355            if (liveChatLastMsgId > 0) url += '?since_id=' + liveChatLastMsgId;
356            url += (url.indexOf('?') === -1 ? '?' : '&') + '_pkb_nocache=' + Date.now();
357            var resp = await fetch(url, {
358                cache: 'no-store',
359                headers: { 'Cache-Control': 'no-cache' }
360            });
361            var data = await resp.json();
362            if (!data.ok) return;
363
364            // Detect status change → "resolved" = end
365            if (data.status && data.status !== liveChatStatus) {
366                var prevStatus = liveChatStatus;
367                liveChatStatus = data.status;
368                if (data.status === 'resolved') {
369                    addSystemMessage('Chat ended.');
370                    showResolvedUI();
371                    stopLiveChatPolling();
372                    return;
373                }
374            }
375
376            // Display new messages
377            var messages = data.messages || [];
378            for (var i = 0; i < messages.length; i++) {
379                var msg = messages[i];
380                if (msg.id <= liveChatLastMsgId) continue;
381                liveChatLastMsgId = msg.id;
382
383                if (msg.sender_type === 'system') {
384                    addSystemMessage(msg.message);
385                } else if (msg.sender_type === 'agent') {
386                    // Agent messages — show name label above + timestamp below
387                    addMessage(msg.message, 'bot', msg.sender_name, msg.created_at);
388                }
389            }
390
391            // Persist latest state so page refresh doesn't lose the session
392            try { sessionStorage.setItem(LIVE_CHAT_KEY, JSON.stringify({ chat_id: liveChatChatId, last_msg_id: liveChatLastMsgId, status: liveChatStatus })); } catch(e) {}
393        } catch (e) {
394            // silent fail on poll errors
395        }
396    }
397
398    async function sendLiveChatMessage(text) {
399        addMessage(text, 'user');
400        setLoading(true);
401        try {
402            await fetch(LIVE_CHAT_REST + '/pkb/v1/live-chat/message', {
403                method: 'POST',
404                headers: { 'Content-Type': 'application/json' },
405                body: JSON.stringify({
406                    chat_id: liveChatChatId,
407                    message: text,
408                    sender_name: getUserInfo().name || 'Customer'
409                })
410            });
411            // Poll will pick up agent reply
412        } catch (e) {
413            addSystemMessage('Failed to send message. Please try again.');
414        } finally {
415            setLoading(false);
416            setTimeout(function() { inputEl.focus(); }, 100);
417        }
418    }
419
420    // --- Core ---
421
422    async function ask(question) {
423        conversation.push({ role: 'user', content: question });
424        addMessage(question, 'user');
425        showTyping();
426        setLoading(true);
427
428        try {
429            var userInfo = getUserInfo();
430            var resp = await fetch(API_URL, {
431                method: 'POST',
432                headers: {
433                    'Content-Type': 'application/json',
434                    'x-api-key': API_KEY,
435                },
436                body: JSON.stringify({
437                    product: PRODUCT,
438                    question: question,
439                    conversation: conversation.slice(-30),
440                    user_name: userInfo.name,
441                    user_email: userInfo.email,
442                    name: userInfo.name,
443                    email: userInfo.email,
444                    customer_name: userInfo.name,
445                    customer_email: userInfo.email,
446                    wp_rest_base: LIVE_CHAT_REST,
447                }),
448            });
449
450            hideTyping();
451
452            if (!resp.ok) {
453                var err = {};
454                try { err = await resp.json(); } catch(e) {}
455                addMessage('Sorry, I couldnt get an answer. ' + (err.error || 'Server error.'));
456                setTimeout(function() { inputEl.focus(); }, 100);
457                return;
458            }
459
460            var data = await resp.json();
461            var answer = data.answer || 'No answer received.';
462
463            conversation.push({ role: 'assistant', content: answer });
464            if (conversation.length > 50) {
465                conversation = conversation.slice(-50);
466            }
467            try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(conversation)); } catch(e) {}
468
469            addMessage(answer, 'bot');
470
471            // If the bridge returned a live chat session, start polling
472            if (data.live_chat && data.live_chat.chat_id) {
473                startLiveChatPolling(data.live_chat.chat_id);
474                addSystemMessage('Waiting for an agent to join...');
475            }
476
477            setTimeout(function() { inputEl.focus(); }, 100);
478
479        } catch (err) {
480            hideTyping();
481            addMessage('Sorry, something went wrong. Check your connection and try again.', 'bot');
482            setTimeout(function() { inputEl.focus(); }, 100);
483            console.error('KB Bot error:', err);
484        } finally {
485            setLoading(false);
486        }
487    }
488
489    function sendMessage() {
490        if (chatResolved) return;
491        var text = inputEl.value.trim();
492        if (!text) return;
493        inputEl.value = '';
494        if (liveChatChatId) {
495            sendLiveChatMessage(text);
496        } else {
497            ask(text);
498        }
499    }
500
501    // --- Event Listeners ---
502
503    toggleBtn.addEventListener('click', function() {
504        container.classList.add('open');
505        hideTeaser();
506        inputEl.focus();
507        scrollToBottom();
508    });
509
510    headerClose.addEventListener('click', function() {
511        container.classList.remove('open');
512        container.classList.remove('expanded');
513        headerCollapse.style.display = 'none';
514        headerExpand.style.display = '';
515    });
516
517    // Expand / Collapse toggle
518    headerExpand.addEventListener('click', function() {
519        container.classList.add('expanded');
520        headerExpand.style.display = 'none';
521        headerCollapse.style.display = '';
522        scrollToBottom();
523    });
524
525    headerCollapse.addEventListener('click', function() {
526        container.classList.remove('expanded');
527        headerCollapse.style.display = 'none';
528        headerExpand.style.display = '';
529        scrollToBottom();
530    });
531
532    // --- Teaser bubble (pre-open nudge) ---
533
534    // Dynamic teasers come from the backend "Teaser Texts" setting via data-teasers.
535    // Each item: { greeting, text }. Fall back to defaults if empty.
536    var teaserData = null;
537    try {
538        teaserData = JSON.parse(container.getAttribute('data-teasers') || 'null');
539    } catch (e) { teaserData = null; }
540    var TEASER_MESSAGES = [];
541    if (Array.isArray(teaserData) && teaserData.length) {
542        for (var ti = 0; ti < teaserData.length; ti++) {
543            var tItem = teaserData[ti] || {};
544            TEASER_MESSAGES.push({
545                greeting: (tItem.greeting || '').toString(),
546                text:     (tItem.text || '').toString()
547            });
548        }
549    }
550    if (!TEASER_MESSAGES.length) {
551        TEASER_MESSAGES = [
552            { greeting: '👋 Hello mate!', text: 'Need help with your WordPress project? 👋🏻' },
553            { greeting: '👋 Hello mate!', text: 'Need WordPress or Full-Stack Development Expertise? 💻' },
554            { greeting: '👋 Hello mate!', text: 'Need a custom solution for your website? 🛠️' },
555            { greeting: '👋 Hello mate!', text: 'Have a question? Let\'s chat! 💬' },
556            { greeting: '👋 Hello mate!', text: 'Share your requirements and get expert guidance. 📝' },
557            { greeting: '👋 Hello mate!', text: 'Have a development challenge? Our experts can help. 😉' }
558        ];
559    }
560
561    var TEASER_DELAY = 6000; // ms before first appearance
562    var TEASER_SWAP_INTERVAL = 8000; // ms between message swaps
563    var teaserTimer = null;
564    var teaserSwapTimer = null;
565    var teaserIdx = -1;
566    var teaserShown = false;
567    var teaserDismissed = false;
568
569    function getTeaserEls() {
570        var teaser = document.getElementById('pkb-teaser');
571        var greetingEl = document.getElementById('pkb-teaser-greeting');
572        var textEl = document.getElementById('pkb-teaser-text');
573        return { teaser: teaser, greetingEl: greetingEl, textEl: textEl };
574    }
575
576    function randomTeaserIndex() {
577        var n = TEASER_MESSAGES.length;
578        var next;
579        do {
580            next = Math.floor(Math.random() * n);
581        } while (next === teaserIdx && n > 1);
582        return next;
583    }
584
585    function setTeaser(teaser) {
586        var els = getTeaserEls();
587        if (!els.textEl) return;
588        if (teaser && typeof teaser === 'object') {
589            if (els.greetingEl && teaser.greeting !== undefined && teaser.greeting !== null) {
590                els.greetingEl.innerHTML = teaser.greeting;
591            }
592            els.textEl.innerHTML = teaser.text;
593        }
594    }
595
596    // Fade out, swap, fade in (used for the rotating message text)
597    function swapTeaserText(teaser) {
598        var els = getTeaserEls();
599        if (!els.textEl) return;
600        els.textEl.classList.remove('pkb-teaser-fade');
601        void els.textEl.offsetWidth; // restart transition
602        els.textEl.classList.add('pkb-teaser-fade');
603        setTimeout(function() {
604            setTeaser(teaser);
605            els.textEl.classList.remove('pkb-teaser-fade');
606        }, 200);
607    }
608
609    function showTeaser() {
610        var els = getTeaserEls();
611        if (!els.teaser) return;
612        if (container.classList.contains('open')) return;
613        if (teaserDismissed) return;
614        teaserShown = true;
615        // Pick a random starting teaser
616        teaserIdx = randomTeaserIndex();
617        setTeaser(TEASER_MESSAGES[teaserIdx]);
618        els.teaser.classList.add('visible');
619        // Start cycling messages if not already
620        if (!teaserSwapTimer) {
621            teaserSwapTimer = setInterval(function() {
622                if (container.classList.contains('open') || teaserDismissed) return;
623                teaserIdx = randomTeaserIndex();
624                swapTeaserText(TEASER_MESSAGES[teaserIdx]);
625            }, TEASER_SWAP_INTERVAL);
626        }
627    }
628
629    function hideTeaser() {
630        var els = getTeaserEls();
631        if (!els.teaser) return;
632        teaserShown = false;
633        els.teaser.classList.remove('visible');
634        if (teaserSwapTimer) {
635            clearInterval(teaserSwapTimer);
636            teaserSwapTimer = null;
637        }
638    }
639
640    function dismissTeaser() {
641        teaserDismissed = true;
642        hideTeaser();
643        try { sessionStorage.setItem('pkb_teaser_dismissed', '1'); } catch(e) {}
644    }
645
646    // Restore dismissal state from sessionStorage (per tab session)
647    try {
648        if (sessionStorage.getItem('pkb_teaser_dismissed') === '1') {
649            teaserDismissed = true;
650        }
651    } catch(e) {}
652
653    // Show teaser after delay if chat not opened
654    teaserTimer = setTimeout(showTeaser, TEASER_DELAY);
655
656    // Click teaser -> open chat (but NOT when clicking the dismiss ✕)
657    document.addEventListener('click', function(e) {
658        var t = e.target;
659        if (t && t.closest && t.closest('#pkb-teaser') && !t.closest('#pkb-teaser-close')) {
660            e.preventDefault();
661            container.classList.add('open');
662            hideTeaser();
663            inputEl.focus();
664            scrollToBottom();
665        }
666    });
667
668    // "Start New Chat" button
669    newChatBtn.addEventListener('click', startNewChat);
670
671    // Dismiss button
672    document.addEventListener('click', function(e) {
673        var t = e.target;
674        if (t && t.closest && t.closest('#pkb-teaser-close')) {
675            e.preventDefault();
676            e.stopPropagation();
677            dismissTeaser();
678        }
679    });
680
681    // Keyboard accessible: Enter/Space on teaser opens chat
682    document.addEventListener('keydown', function(e) {
683        if ((e.key === 'Enter' || e.key === ' ') && e.target && e.target.id === 'pkb-teaser') {
684            e.preventDefault();
685            container.classList.add('open');
686            hideTeaser();
687            inputEl.focus();
688            scrollToBottom();
689        }
690    });
691
692
693    // Auto-resize textarea as user types
694    inputEl.addEventListener('input', function() {
695        inputEl.style.height = 'auto';
696        inputEl.style.height = Math.min(inputEl.scrollHeight, 100) + 'px';
697    });
698
699    sendBtn.addEventListener('click', sendMessage);
700
701    inputEl.addEventListener('keydown', function(e) {
702        if (e.key === 'Enter' && !e.shiftKey) {
703            e.preventDefault();
704            sendMessage();
705            // Reset height after sending
706            inputEl.style.height = 'auto';
707        }
708    });
709
710    // Restore conversation history + live chat session on page refresh
711    // Live chat messages are fetched from server (not sessionStorage) so they survive refresh
712    (async function restoreLiveChat() {
713        if (!liveChatChatId || !LIVE_CHAT_REST) {
714            if (chatResolved) showResolvedUI();
715            return;
716        }
717        try {
718            var url = LIVE_CHAT_REST + '/pkb/v1/live-chat/' + liveChatChatId + '/messages';
719            url += '?_pkb_nocache=' + Date.now();
720            var resp = await fetch(url, {
721                cache: 'no-store',
722                headers: { 'Cache-Control': 'no-cache' }
723            });
724            var data = await resp.json();
725            if (!data.ok || data.status === 'resolved') {
726                // Replay messages so the visitor can still see chat history after refresh
727                if (data.messages) {
728                    var rMsgs = data.messages;
729                    var rWelcome = messagesEl.querySelector('.pkb-bot');
730                    if (rWelcome) rWelcome.remove();
731                    for (var ri = 0; ri < rMsgs.length; ri++) {
732                        var rm = rMsgs[ri];
733                        if (rm.sender_type === 'system') addSystemMessage(rm.message);
734                        else if (rm.sender_type === 'agent') addMessage(rm.message, 'bot', rm.sender_name, rm.created_at);
735                        else if (rm.sender_type === 'customer') addMessage(rm.message, 'user');
736                    }
737                }
738                if (data.status === 'resolved') showResolvedUI();
739                stopLiveChatPolling();
740                return;
741            }
742
743            liveChatStatus = data.status || null;
744            var messages = data.messages || [];
745            // Remove the welcome greeting — live chat messages are the real history
746            var welcomeMsg = messagesEl.querySelector('.pkb-bot');
747            if (welcomeMsg) welcomeMsg.remove();
748            // Replay all previous messages into the UI
749            for (var i = 0; i < messages.length; i++) {
750                var msg = messages[i];
751                if (msg.sender_type === 'system') {
752                    addSystemMessage(msg.message);
753                } else if (msg.sender_type === 'agent') {
754                    addMessage(msg.message, 'bot', msg.sender_name);
755                } else if (msg.sender_type === 'customer') {
756                    addMessage(msg.message, 'user');
757                }
758                liveChatLastMsgId = Math.max(liveChatLastMsgId, parseInt(msg.id, 10) || 0);
759            }
760
761            // Restart polling from where we left off
762            try { sessionStorage.setItem(LIVE_CHAT_KEY, JSON.stringify({ chat_id: liveChatChatId, last_msg_id: liveChatLastMsgId, status: liveChatStatus })); } catch(e) {}
763            if (!liveChatPollTimer) {
764                liveChatPollTimer = setInterval(liveChatPoll, 5000);
765            }
766        } catch(e) { /* silent */ }
767    })();
768
769    // Restore conversation history
770    if (conversation.length > 0) {
771        var welcomeMsg = messagesEl.querySelector('.pkb-bot');
772        if (welcomeMsg) welcomeMsg.remove();
773        for (var i = 0; i < conversation.length; i++) {
774            var role = conversation[i].role === 'assistant' ? 'bot' : conversation[i].role;
775            addMessage(conversation[i].content, role);
776        }
777    }
778
779    console.log('KB Bot loaded (product: ' + PRODUCT + ')');
780})();

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.