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, '&') 165 .replace(/</g, '<') 166 .replace(/>/g, '>') 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.