1(function () { 2 'use strict'; 3 4 function getStorageNamespace() { 5 var floatingConfig = (typeof listeoAiFloatingChatConfig !== 'undefined') 6 ? listeoAiFloatingChatConfig 7 : {}; 8 var chatConfig = (typeof listeoAiChatConfig !== 'undefined') 9 ? listeoAiChatConfig 10 : {}; 11 var namespace = chatConfig.storageNamespace || floatingConfig.storageNamespace || ''; 12 13 return namespace ? String(namespace) : ''; 14 } 15 16 function getScopedStorageKey(key) { 17 var namespace = getStorageNamespace(); 18 return namespace ? key + '_' + namespace : key; 19 } 20 21 var STORAGE_KEY_BUBBLE_DISMISSED = getScopedStorageKey( 22 'listeo_floating_chat_bubble_dismissed' 23 ); 24 var STORAGE_KEY_CHAT_OPENED = getScopedStorageKey('listeo_floating_chat_opened'); 25 var STORAGE_KEY_USER_INTERACTED = getScopedStorageKey( 26 'listeo_floating_chat_interacted' 27 ); 28 var STORAGE_KEY_PROACTIVE_COOLDOWN = getScopedStorageKey( 29 'listeo_floating_proactive_cooldown' 30 ); 31 var FADE_DURATION_MS = 300; 32 var MINI_CHAT_ANIMATION_MS = 220; 33 34 function debugLog() { 35 if (typeof listeoAiChatConfig !== 'undefined' && listeoAiChatConfig.debugMode) { 36 var args = Array.prototype.slice.call(arguments); 37 args.unshift('[AI Chat Widget]'); 38 console.log.apply(console, args); 39 } 40 } 41 42 function dispatchReady(chatId) { 43 if (typeof window.jQuery !== 'undefined') { 44 try { 45 window.jQuery(document).trigger('listeo-floating-chat-ready', { chatId: chatId }); 46 } catch (e) {} 47 } 48 try { 49 document.dispatchEvent(new CustomEvent('listeo-floating-chat-ready', { 50 detail: { chatId: chatId } 51 })); 52 } catch (e) {} 53 } 54 55 function dispatchWidgetState(popup, eventName, chatId) { 56 if (!popup) return; 57 try { 58 popup.dispatchEvent(new CustomEvent(eventName, { 59 bubbles: true, 60 detail: { chatId: chatId } 61 })); 62 } catch (e) {} 63 } 64
65 function ListeoFloatingChatWidget() { 66 this.button = document.getElementById('listeo-floating-chat-button'); 67 this.popup = document.getElementById('listeo-floating-chat-popup'); 68 this.bubbleStack = document.getElementById('listeo-floating-bubble-stack'); 69 this.welcomeBubble = document.getElementById('listeo-floating-welcome-bubble'); 70 this.iconOpen = document.querySelector('.listeo-floating-icon-open'); 71 this.iconClose = document.querySelector('.listeo-floating-icon-close'); 72 this.isOpen = false; 73 this.chatInitialized = false; 74 this.scriptsLoaded = false; 75 this.closeTimeoutId = null; 76 this.proactiveTimeoutIds = []; 77 this.proactiveScrollActions = []; 78 this.proactiveScrollHandler = null; 79 this.proactiveActionQueue = []; 80 this.proactiveQueueSequence = 0; 81 this.proactiveQueueProcessTimeoutId = null; 82 this.activeProactiveActionId = ''; 83 this.activeProactiveActionType = ''; 84 this.pendingProactiveMessages = []; 85 this.proactiveMessageTimeoutId = null; 86 this.proactiveMessageAttempts = 0; 87 this.miniChat = null; 88 this.miniChatMessage = null; 89 this.miniChatQuickActions = null; 90 this.miniChatInput = null; 91 this.miniChatCloseTimeoutId = null; 92 this.pendingMagicQuestion = null; 93 this.magicQuestionTimeoutId = null; 94 this.magicQuestionAttempts = 0; 95 96 var cfg = (typeof listeoAiFloatingChatConfig !== 'undefined') ? listeoAiFloatingChatConfig : {}; 97 var chatCfg = (typeof listeoAiChatConfig !== 'undefined') ? listeoAiChatConfig : {}; 98 this.lazyScripts = (cfg && cfg.lazyScripts) ? cfg.lazyScripts : []; 99 this.scriptVersion = (cfg && cfg.scriptVersion) ? cfg.scriptVersion : ''; 100 this.assistantName = String(chatCfg.chatName || '').trim(); 101 this.assistantAvatarUrl = String(chatCfg.chatAvatarUrl || '').trim(); 102 this.proactiveActions = (cfg && Array.isArray(cfg.proactiveActions)) 103 ? cfg.proactiveActions 104 : ((cfg && cfg.proactiveAction) ? [cfg.proactiveAction] : []); 105 this.proactiveStrings = (cfg && cfg.proactiveStrings) ? cfg.proactiveStrings : {}; 106 var proactiveSettings = (cfg && cfg.proactiveSettings) 107 ? cfg.proactiveSettings 108 : {}; 109 var configuredCooldown = proactiveSettings.interaction_cooldown || 110 cfg.proactiveCooldown; 111 var configuredDisplayMode = proactiveSettings.display_mode || 112 cfg.proactiveDisplayMode; 113 this.proactiveCooldown = [ 114 'none', 115 'session', 116 '30_minutes', 117 '1_hour', 118 '24_hours', 119 '7_days' 120 ].indexOf(String(configuredCooldown || '')) !== -1 121 ? String(configuredCooldown) 122 : 'session'; 123 this.proactiveDisplayMode = String(configuredDisplayMode || '') === 'highest_priority' 124 ? 'highest_priority' 125 : 'all'; 126 this.hasUserInteracted = this.isProactiveCooldownActive(); 127 128 this.init(); 129 } 130
131 ListeoFloatingChatWidget.prototype.init = function () { 132 this.checkWelcomeBubbleStatus(); 133 this.bindEvents(); 134 this.bindProactiveStorageSync(); 135 this.restoreChatState(); 136 this.scheduleProactiveActions(); 137 debugLog('Widget initialized', this.lazyScripts.length > 0 ? '(lazy load enabled)' : ''); 138 }; 139 140 ListeoFloatingChatWidget.prototype.checkWelcomeBubbleStatus = function () { 141 if (!this.welcomeBubble) return; 142 var dismissed = localStorage.getItem(STORAGE_KEY_BUBBLE_DISMISSED); 143 if (dismissed === 'true') { 144 this.welcomeBubble.classList.add('hidden'); 145 } else { 146 this.welcomeBubble.classList.remove('hidden'); 147 } 148 }; 149 150 ListeoFloatingChatWidget.prototype.restoreChatState = function () { 151 var cfg = (typeof listeoAiFloatingChatConfig !== 'undefined') ? listeoAiFloatingChatConfig : {}; 152 if (!cfg.keepChatOpened) return; 153 154 var wasOpen = localStorage.getItem(STORAGE_KEY_CHAT_OPENED); 155 if (wasOpen === 'true') { 156 if (this.popup) { 157 this.popup.classList.add('listeo-no-animation'); 158 } 159 this.openChat({ restore: true }); 160 debugLog('Restored chat open state from localStorage'); 161 } 162 }; 163
164 ListeoFloatingChatWidget.prototype.bindEvents = function () { 165 var self = this; 166 167 if (this.button) { 168 this.button.addEventListener('click', function (e) { 169 e.preventDefault(); 170 self.toggleChat(); 171 }); 172 } 173 174 if (this.bubbleStack) { 175 this.bubbleStack.addEventListener('click', function (e) { 176 if (!e.target || typeof e.target.closest !== 'function') return; 177 178 var bubble = e.target.closest('.listeo-floating-welcome-bubble'); 179 if (!bubble || !self.bubbleStack.contains(bubble)) return; 180 181 e.stopPropagation(); 182 var openProactiveBubble = bubble.classList.contains( 183 'listeo-floating-proactive-bubble' 184 ); 185 self.dismissWelcomeBubble(); 186 if (openProactiveBubble) { 187 self.openChat(); 188 } 189 }); 190 } 191 192 document.addEventListener('click', function (e) { 193 if (!e.target || typeof e.target.closest !== 'function') return; 194 195 var magicLink = e.target.closest('[data-chat-magic-link]'); 196 if (!magicLink) return; 197 198 e.preventDefault(); 199 self.activateMagicLink(magicLink); 200 }); 201 202 document.addEventListener('listeo-floating-chat-ready', function () { 203 self.sendPendingMagicQuestion(); 204 self.showPendingProactiveMessages(); 205 }); 206 207 document.addEventListener('listeo-ai-chat-user-message-sent', function (e) { 208 if (self.popup && e.target && self.popup.contains(e.target)) { 209 self.markUserInteracted(); 210 } 211 }); 212 213 document.addEventListener('keydown', function (e) { 214 if (e.key === 'Escape' && self.isOpen) { 215 self.closeChat(); 216 } else if ( 217 e.key === 'Escape' && 218 self.miniChat && 219 self.miniChat.classList.contains('is-visible') 220 ) { 221 self.dismissActiveMiniChat(); 222 } 223 }); 224 }; 225
226 ListeoFloatingChatWidget.prototype.toggleChat = function () { 227 if (this.isOpen) { 228 this.closeChat(); 229 } else { 230 if (this.popup) { 231 this.popup.classList.remove('listeo-no-animation'); 232 } 233 this.openChat(); 234 } 235 }; 236 237 ListeoFloatingChatWidget.prototype.activateMagicLink = function (magicLink) { 238 var question = (magicLink.getAttribute('data-chat-magic-link') || '').trim(); 239 240 this.pendingMagicQuestion = question ? question.substring(0, 1000) : null; 241 this.magicQuestionAttempts = 0; 242 243 if (this.magicQuestionTimeoutId) { 244 clearTimeout(this.magicQuestionTimeoutId); 245 this.magicQuestionTimeoutId = null; 246 } 247 248 if (!this.isOpen) { 249 this.openChat(); 250 } 251 252 if (question) { 253 this.sendPendingMagicQuestion(); 254 } 255 }; 256 257 ListeoFloatingChatWidget.prototype.sendPendingMagicQuestion = function () { 258 var self = this; 259 if (!this.pendingMagicQuestion) return; 260 261 var chatWrapper = document.getElementById('listeo-floating-chat-instance'); 262 var chatInstance = null; 263 264 if (chatWrapper && typeof window.jQuery !== 'undefined') { 265 chatInstance = window.jQuery(chatWrapper).data('listeo-ai-chat-instance'); 266 } 267 268 if (chatInstance && typeof chatInstance.sendMagicMessage === 'function') { 269 var question = this.pendingMagicQuestion; 270 this.pendingMagicQuestion = null; 271 this.magicQuestionAttempts = 0; 272 273 if (this.magicQuestionTimeoutId) { 274 clearTimeout(this.magicQuestionTimeoutId); 275 this.magicQuestionTimeoutId = null; 276 } 277 278 chatInstance.sendMagicMessage(question); 279 return; 280 } 281 282 if (!this.magicQuestionTimeoutId && this.magicQuestionAttempts < 100) { 283 this.magicQuestionAttempts++; 284 this.magicQuestionTimeoutId = setTimeout(function () { 285 self.magicQuestionTimeoutId = null; 286 self.sendPendingMagicQuestion(); 287 }, 50); 288 } 289 }; 290
291 ListeoFloatingChatWidget.prototype.openChat = function (options) { 292 var self = this; 293 options = options || {}; 294 295 this.hideMiniChat(true); 296 this.clearPendingProactiveActions(); 297 this.activeProactiveActionId = ''; 298 this.activeProactiveActionType = ''; 299 300 if (options.proactive && options.message) { 301 this.queueProactiveMessage(options.actionId, options.message); 302 } 303 this.dismissWelcomeBubble(); 304 305 if (typeof ListeoSilkWave !== 'undefined') { 306 ListeoSilkWave.start(); 307 } 308 309 if (this.closeTimeoutId) { 310 clearTimeout(this.closeTimeoutId); 311 this.closeTimeoutId = null; 312 } 313 314 if (this.popup) { 315 this.popup.style.opacity = ''; 316 this.popup.style.transition = ''; 317 this.popup.style.display = 'block'; 318 setTimeout(function () { self.scrollToBottom(); }, FADE_DURATION_MS); 319 } 320 321 if (this.iconOpen) this.iconOpen.style.display = 'none'; 322 if (this.iconClose) this.iconClose.style.display = ''; 323 324 this.isOpen = true; 325 dispatchWidgetState(this.popup, 'listeo-floating-chat-opened', 'listeo-floating-chat-instance'); 326 327 if (!this.chatInitialized) { 328 this.chatInitialized = true; 329 if (this.lazyScripts.length > 0 && !this.scriptsLoaded) { 330 this.lazyLoadAndInit(); 331 } else { 332 this.initializeChat(); 333 } 334 } 335 336 if (options.proactive && options.message) { 337 this.showPendingProactiveMessages(); 338 } 339 340 try { 341 localStorage.setItem(STORAGE_KEY_CHAT_OPENED, 'true'); 342 } catch (e) {} 343 344 debugLog('Chat opened'); 345 }; 346
347 ListeoFloatingChatWidget.prototype.scrollToBottom = function () { 348 var messagesContainer = document.getElementById('listeo-floating-chat-instance-messages'); 349 if (messagesContainer && messagesContainer.scrollHeight) { 350 messagesContainer.scrollTop = messagesContainer.scrollHeight; 351 } 352 }; 353 354 ListeoFloatingChatWidget.prototype.closeChat = function () { 355 var self = this; 356 this.isOpen = false; 357 dispatchWidgetState(this.popup, 'listeo-floating-chat-closed', 'listeo-floating-chat-instance'); 358 359 if (this.popup) { 360 this.popup.style.transition = 'opacity ' + FADE_DURATION_MS + 'ms'; 361 this.popup.style.opacity = '0'; 362 363 this.closeTimeoutId = setTimeout(function () { 364 // reopen guard 365 if (!self.isOpen && self.popup) { 366 self.popup.style.display = 'none'; 367 self.popup.style.opacity = ''; 368 self.popup.style.transition = ''; 369 } 370 self.closeTimeoutId = null; 371 372 if (!self.isOpen && !self.hasUserInteracted) { 373 if (self.bubbleStack) { 374 var proactiveBubbles = self.bubbleStack.querySelectorAll( 375 '.listeo-floating-proactive-bubble' 376 ); 377 Array.prototype.forEach.call(proactiveBubbles, function (bubble) { 378 bubble.remove(); 379 }); 380 } 381 self.scheduleProactiveActions(); 382 } 383 }, FADE_DURATION_MS); 384 } 385 386 if (typeof ListeoSilkWave !== 'undefined') { 387 ListeoSilkWave.stop(); 388 } 389 390 if (this.iconClose) this.iconClose.style.display = 'none'; 391 if (this.iconOpen) this.iconOpen.style.display = ''; 392 393 try { 394 localStorage.removeItem(STORAGE_KEY_CHAT_OPENED); 395 } catch (e) {} 396 397 debugLog('Chat closed'); 398 }; 399
400 ListeoFloatingChatWidget.prototype.dismissWelcomeBubble = function () { 401 var bubbles = this.bubbleStack 402 ? this.bubbleStack.querySelectorAll('.listeo-floating-welcome-bubble') 403 : (this.welcomeBubble ? [this.welcomeBubble] : []); 404 405 Array.prototype.forEach.call(bubbles, function (bubble) { 406 if (bubble.classList.contains('hidden')) return; 407 408 bubble.style.transition = 'opacity 200ms'; 409 bubble.style.opacity = '0'; 410 411 if (bubble.listeoDismissTimeoutId) { 412 clearTimeout(bubble.listeoDismissTimeoutId); 413 } 414 bubble.listeoDismissTimeoutId = setTimeout(function () { 415 bubble.classList.add('hidden'); 416 bubble.style.opacity = ''; 417 bubble.style.transition = ''; 418 bubble.listeoDismissTimeoutId = null; 419 }, 200); 420 }); 421 422 try { 423 localStorage.setItem(STORAGE_KEY_BUBBLE_DISMISSED, 'true'); 424 } catch (e) {} 425 }; 426 427 ListeoFloating
427ChatWidget.prototype.getProactiveCooldownDuration = function () { 428 var durations = { 429 '30_minutes': 30 * 60 * 1000, 430 '1_hour': 60 * 60 * 1000, 431 '24_hours': 24 * 60 * 60 * 1000, 432 '7_days': 7 * 24 * 60 * 60 * 1000 433 }; 434 435 return durations[this.proactiveCooldown] || 0; 436 }; 437 438 ListeoFloatingChatWidget.prototype.getProactiveActionCooldownKey = function (actionId) { 439 return getScopedStorageKey( 440 'listeo_floating_proactive_action_cooldown_' + String(actionId || '') 441 ); 442 }; 443 444 ListeoFloatingChatWidget.prototype.isProactiveCooldownKeyActive = function (key) { 445 if (this.proactiveCooldown === 'none') return false; 446 447 if (this.proactiveCooldown === 'session') { 448 try { 449 return sessionStorage.getItem(key) === 'true'; 450 } catch (e) { 451 return false; 452 } 453 } 454 455 try { 456 var expiresAt = parseInt(localStorage.getItem(key), 10); 457 if (Number.isFinite(expiresAt) && expiresAt > Date.now()) { 458 return true; 459 } 460 localStorage.removeItem(key); 461 } catch (e) {} 462 463 return false; 464 }; 465
466 ListeoFloatingChatWidget.prototype.isProactiveCooldownActive = function () { 467 var key = this.proactiveCooldown === 'session' 468 ? STORAGE_KEY_USER_INTERACTED 469 : STORAGE_KEY_PROACTIVE_COOLDOWN; 470 471 return this.isProactiveCooldownKeyActive(key); 472 }; 473 474 ListeoFloatingChatWidget.prototype.isProactiveActionCooldownActive = function (actionId) { 475 return this.isProactiveCooldownKeyActive( 476 this.getProactiveActionCooldownKey(actionId) 477 ); 478 }; 479 480 ListeoFloatingChatWidget.prototype.setProactiveCooldownKey = function (key) { 481 if (this.proactiveCooldown === 'none') return; 482 483 if (this.proactiveCooldown === 'session') { 484 try { 485 sessionStorage.setItem(key, 'true'); 486 } catch (e) {} 487 return; 488 } 489 490 var duration = this.getProactiveCooldownDuration(); 491 if (!duration) return; 492 493 try { 494 localStorage.setItem(key, String(Date.now() + duration)); 495 } catch (e) {} 496 }; 497
498 ListeoFloatingChatWidget.prototype.clearPendingProactiveActions = function () { 499 this.proactiveTimeoutIds.forEach(function (timeoutId) { 500 clearTimeout(timeoutId); 501 }); 502 this.proactiveTimeoutIds = []; 503 this.proactiveScrollActions = []; 504 this.proactiveActionQueue = []; 505 if (this.proactiveQueueProcessTimeoutId) { 506 clearTimeout(this.proactiveQueueProcessTimeoutId); 507 this.proactiveQueueProcessTimeoutId = null; 508 } 509 this.removeProactiveScrollListener(); 510 }; 511 512 ListeoFloatingChatWidget.prototype.markUserInteracted = function () { 513 this.hasUserInteracted = true; 514 this.clearPendingProactiveActions(); 515 this.dismissWelcomeBubble(); 516 this.setProactiveCooldownKey( 517 this.proactiveCooldown === 'session' 518 ? STORAGE_KEY_USER_INTERACTED 519 : STORAGE_KEY_PROACTIVE_COOLDOWN 520 ); 521 }; 522 523 ListeoFloatingChatWidget.prototype.dismissActiveMiniChat = function () { 524 var actionId = this.activeProactiveActionId; 525 526 if (actionId) { 527 this.setProactiveCooldownKey( 528 this.getProactiveActionCooldownKey(actionId) 529 ); 530 } 531 532 this.activeProactiveActionId = ''; 533 this.activeProactiveActionType = ''; 534 this.hideMiniChat(true, true); 535 this.processProactiveActionQueue(); 536 }; 537
538 ListeoFloatingChatWidget.prototype.bindProactiveStorageSync = function () { 539 var self = this; 540 if (!this.getProactiveCooldownDuration()) return; 541 542 window.addEventListener('storage', function (event) { 543 if (!event.key) return; 544 545 if ( 546 event.key === STORAGE_KEY_PROACTIVE_COOLDOWN && 547 self.isProactiveCooldownActive() 548 ) { 549 self.hasUserInteracted = true; 550 self.clearPendingProactiveActions(); 551 if (self.activeProactiveActionType === 'mini_chat') { 552 self.activeProactiveActionId = ''; 553 self.activeProactiveActionType = ''; 554 self.hideMiniChat(true); 555 } 556 self.dismissWelcomeBubble(); 557 return; 558 } 559 560 var activeActionId = self.activeProactiveActionId; 561 if ( 562 activeActionId && 563 event.key === self.getProactiveActionCooldownKey(activeActionId) && 564 self.isProactiveActionCooldownActive(activeActionId) 565 ) { 566 self.activeProactiveActionId = ''; 567 self.activeProactiveActionType = ''; 568 self.hideMiniChat(true); 569 self.processProactiveActionQueue(); 570 } 571 572 if (event.newValue) { 573 self.proactiveActionQueue = self.proactiveActionQueue.filter(function (item) { 574 return event.key !== self.getProactiveActionCooldownKey(item.actionId); 575 }); 576 } 577 }); 578 }; 579
580 ListeoFloatingChatWidget.prototype.scheduleProactiveActions = function () { 581 var self = this; 582 583 if (this.hasUserInteracted || this.isOpen) return; 584 585 var actions = this.proactiveActions.filter(function (action) { 586 return action && 587 !self.isProactiveActionCooldownActive(String(action.id || '').trim()); 588 }); 589 if (this.proactiveDisplayMode === 'highest_priority') { 590 actions.sort(function (a, b) { 591 return self.getProactiveActionPriority(a) - 592 self.getProactiveActionPriority(b); 593 }); 594 actions = actions.slice(0, 1); 595 } 596 597 actions.forEach(function (action) { 598 self.scheduleProactiveAction(action); 599 }); 600 }; 601 602 ListeoFloatingChatWidget.prototype.scheduleProactiveAction = function (action) { 603 var self = this; 604 605 if (!action || this.hasUserInteracted) return; 606 607 var actionId = String(action.id || '').trim(); 608 var actionType = String(action.action || ''); 609 var trigger = String(action.trigger || '') === 'scroll_depth' 610 ? 'scroll_depth' 611 : 'time'; 612 613 if ( 614 !actionId || 615 ( 616 actionType !== 'show_bubble' && 617 actionType !== 'open_chat' && 618 actionType !== 'mini_chat' 619 ) 620 ) { 621 return; 622 } 623 if (this.isProactiveActionCooldownActive(actionId)) return; 624 625 var firedKey = getScopedStorageKey( 626 'listeo_floating_proactive_action_' + actionId 627 ); 628 629 try { 630 var storedStatus = sessionStorage.getItem(firedKey); 631 if (storedStatus === 'shown' || storedStatus === 'true') { 632 sessionStorage.removeItem(firedKey); 633 } 634 } catch (e) {} 635 636 if (trigger === 'scroll_depth') { 637 var scrollDepth = parseInt(action.scroll_depth, 10); 638 if (!Number.isFinite(scrollDepth) || scrollDepth < 1 || scrollDepth > 100) { 639 return; 640 } 641 642 this.proactiveScrollActions.push({ 643 action: action, 644 actionId: actionId, 645 actionType: actionType, 646 scrollDepth: scrollDepth 647 }); 648 this.addProactiveScrollListener(); 649 this.checkProactiveScrollActions(); 650 return; 651 } 652 653 var delay = parseInt(action.delay, 10); 654 if (!Number.isFinite(delay) || delay < 0) return; 655 656 var timeoutId = setTimeout(function () { 657 self.proactiveTimeoutIds = self.proactiveTimeoutIds.filter(function (id) { 658 return id !== timeoutId; 659 }); 660 if (self.hasUserInteracted) return; 661 662 self.handleReadyProactiveAction(action, actionId, actionType); 663 }, delay * 1000); 664 665 this.proactiveTimeoutIds.push(timeoutId); 666 }; 667
668 ListeoFloatingChatWidget.prototype.getProactiveActionPriority = function (action) { 669 var priority = parseInt(action && action.priority, 10); 670 return Number.isFinite(priority) ? priority : 10; 671 }; 672 673 ListeoFloatingChatWidget.prototype.handleReadyProactiveAction = function ( 674 action, 675 actionId, 676 actionType 677 ) { 678 if ( 679 this.hasUserInteracted || 680 this.isProactiveActionCooldownActive(actionId) 681 ) { 682 return; 683 } 684 685 if (actionType === 'show_bubble') { 686 this.executeProactiveAction(action, actionId, actionType); 687 return; 688 } 689 690 if (this.proactiveActionQueue.some(function (item) { 691 return item.actionId === actionId; 692 })) { 693 return; 694 } 695 696 this.proactiveActionQueue.push({ 697 action: action, 698 actionId: actionId, 699 actionType: actionType, 700 priority: this.getProactiveActionPriority(action), 701 sequence: this.proactiveQueueSequence++ 702 }); 703 this.proactiveActionQueue.sort(function (a, b) { 704 return a.priority === b.priority 705 ? a.sequence - b.sequence 706 : a.priority - b.priority; 707 }); 708 this.scheduleProactiveActionQueue(); 709 }; 710
711 ListeoFloatingChatWidget.prototype.scheduleProactiveActionQueue = function () { 712 var self = this; 713 if (this.proactiveQueueProcessTimeoutId) return; 714 715 this.proactiveQueueProcessTimeoutId = setTimeout(function () { 716 self.proactiveQueueProcessTimeoutId = null; 717 self.processProactiveActionQueue(); 718 }, 0); 719 }; 720 721 ListeoFloatingChatWidget.prototype.processProactiveActionQueue = function () { 722 if (this.hasUserInteracted || this.isOpen) { 723 this.proactiveActionQueue = []; 724 return; 725 } 726 if ( 727 this.activeProactiveActionType === 'mini_chat' && 728 this.miniChat && 729 this.miniChat.classList.contains('is-visible') 730 ) { 731 return; 732 } 733 734 var item = null; 735 while (this.proactiveActionQueue.length && !item) { 736 var candidate = this.proactiveActionQueue.shift(); 737 if (!this.isProactiveActionCooldownActive(candidate.actionId)) { 738 item = candidate; 739 } 740 } 741 if (!item) return; 742 743 this.executeProactiveAction(item.action, item.actionId, item.actionType); 744 }; 745
746 ListeoFloatingChatWidget.prototype.executeProactiveAction = function ( 747 action, 748 actionId, 749 actionType 750 ) { 751 if ( 752 this.hasUserInteracted || 753 this.isProactiveActionCooldownActive(actionId) 754 ) { 755 return; 756 } 757 758 if (actionType === 'open_chat') { 759 this.openChat({ 760 proactive: true, 761 actionId: actionId, 762 message: String(action.message || '') 763 }); 764 return; 765 } 766 767 if (actionType === 'mini_chat') { 768 if (this.showMiniChat( 769 actionId, 770 String(action.message || ''), 771 action.quick_actions 772 )) { 773 this.activeProactiveActionId = actionId; 774 this.activeProactiveActionType = 'mini_chat'; 775 } else { 776 this.processProactiveActionQueue(); 777 } 778 return; 779 } 780 781 this.showProactiveBubble( 782 actionId, 783 String(action.message || ''), 784 this.getProactiveActionPriority(action) 785 ); 786 }; 787
788 ListeoFloatingChatWidget.prototype.addProactiveScrollListener = function () { 789 var self = this; 790 if (this.proactiveScrollHandler) return; 791 792 this.proactiveScrollHandler = function () { 793 self.checkProactiveScrollActions(); 794 }; 795 window.addEventListener('scroll', this.proactiveScrollHandler, { passive: true }); 796 }; 797 798 ListeoFloatingChatWidget.prototype.removeProactiveScrollListener = function () { 799 if (!this.proactiveScrollHandler) return; 800 801 window.removeEventListener('scroll', this.proactiveScrollHandler); 802 this.proactiveScrollHandler = null; 803 }; 804 805 ListeoFloatingChatWidget.prototype.getPageScrollPercentage = function () { 806 var documentElement = document.documentElement; 807 var body = document.body; 808 var scrollTop = window.pageYOffset || 809 (documentElement ? documentElement.scrollTop : 0) || 810 (body ? body.scrollTop : 0); 811 var viewportHeight = window.innerHeight || 812 (documentElement ? documentElement.clientHeight : 0); 813 var scrollHeight = Math.max( 814 body ? body.scrollHeight : 0, 815 body ? body.offsetHeight : 0, 816 documentElement ? documentElement.clientHeight : 0, 817 documentElement ? documentElement.scrollHeight : 0, 818 documentElement ? documentElement.offsetHeight : 0 819 ); 820 var scrollableHeight = scrollHeight - viewportHeight; 821 822 if (scrollableHeight <= 0) return 0; 823 824 return Math.min(100, Math.max(0, (scrollTop / scrollableHeight) * 100)); 825 }; 826
827 ListeoFloatingChatWidget.prototype.checkProactiveScrollActions = function () { 828 var self = this; 829 830 if (this.hasUserInteracted) { 831 this.proactiveScrollActions = []; 832 this.removeProactiveScrollListener(); 833 return; 834 } 835 836 var scrollPercentage = this.getPageScrollPercentage(); 837 var readyActions = []; 838 839 this.proactiveScrollActions = this.proactiveScrollActions.filter(function (item) { 840 if (scrollPercentage >= item.scrollDepth) { 841 readyActions.push(item); 842 return false; 843 } 844 return true; 845 }); 846 847 if (!this.proactiveScrollActions.length) { 848 this.removeProactiveScrollListener(); 849 } 850 851 readyActions.forEach(function (item) { 852 self.handleReadyProactiveAction(item.action, item.actionId, item.actionType); 853 }); 854 }; 855 856 ListeoFloatingChatWidget.prototype.queueProactiveMessage = function (actionId, message) { 857 actionId = String(actionId || '').trim(); 858 message = String(message || '').trim(); 859 860 if (!actionId || !message) return; 861 862 this.pendingProactiveMessages.push({ 863 id: actionId, 864 message: message 865 }); 866 this.proactiveMessageAttempts = 0; 867 }; 868
869 ListeoFloatingChatWidget.prototype.showPendingProactiveMessages = function () { 870 var self = this; 871 if (!this.pendingProactiveMessages.length) return; 872 873 var chatWrapper = document.getElementById('listeo-floating-chat-instance'); 874 var messagesContainer = document.getElementById('listeo-floating-chat-instance-messages'); 875 var chatInstance = null; 876 877 if (chatWrapper && typeof window.jQuery !== 'undefined') { 878 chatInstance = window.jQuery(chatWrapper).data('listeo-ai-chat-instance'); 879 } 880 881 if (!messagesContainer || !chatInstance || typeof chatInstance.addMessage !== 'function') { 882 if (!this.proactiveMessageTimeoutId && this.proactiveMessageAttempts < 100) { 883 this.proactiveMessageAttempts++; 884 this.proactiveMessageTimeoutId = setTimeout(function () { 885 self.proactiveMessageTimeoutId = null; 886 self.showPendingProactiveMessages(); 887 }, 50); 888 } 889 return; 890 } 891 892 var normalizeText = function (value) { 893 return String(value || '').replace(/\s+/g, ' ').trim(); 894 }; 895 var chatConfig = (typeof listeoAiChatConfig !== 'undefined') ? listeoAiChatConfig : {}; 896 var welcomeMessage = chatConfig.strings ? chatConfig.strings.welcomeMessage : ''; 897 var welcomeHolder = document.createElement('div'); 898 welcomeHolder.innerHTML = welcomeMessage; 899 var welcomeText = normalizeText(welcomeHolder.textContent); 900 var messagesChanged = false; 901 902 this.pendingProactiveMessages.forEach(function (item) { 903 var messageId = 'listeo-proactive-message-' + item.id; 904 if (document.getElementById(messageId)) return; 905 906 var hasConversation = messagesContainer.querySelector( 907 '.listeo-ai-chat-message-user, .listeo-ai-chat-message-assistant' 908 ); 909 var hasProactiveMessage = messagesContainer.querySelector( 910 '[data-proactive-action-id]' 911 ); 912 var systemMessages = messagesContainer.querySelectorAll( 913 '.listeo-ai-chat-message-system:not([data-proactive-action-id])' 914 ); 915 var defaultMessage = null; 916 917 Array.prototype.some.call(systemMessages, function (systemMessage) { 918 var systemContent = systemMessage.querySelector( 919 '.listeo-ai-chat-message-content' 920 ); 921 if ( 922 systemContent && 923 welcomeText && 924 normalizeText(systemContent.textContent) === welcomeText 925 ) { 926 defaultMessage = systemMessage; 927 return true; 928 } 929 return false; 930 }); 931 932 if (!hasConversation && !hasProactiveMessage && defaultMessage) { 933 var content = defaultMessage.querySelector('.listeo-ai-chat-message-content'); 934 if (content) { 935 content.textContent = item.message; 936 defaultMessage.id = messageId; 937 defaultMessage.setAttribute('data-proactive-action-id', item.id); 938 messagesChanged = true; 939 return; 940 } 941 } 942 943 var imageWelcome = messagesContainer.querySelector('.chat-image-bg-welcome-text'); 944 if (!hasConversation && !hasProactiveMessage && imageWelcome) { 945 imageWelcome.remove(); 946 messagesChanged = true; 947 } 948 949 chatInstance.addMessage('system', item.message, messageId, true); 950 messagesChanged = true; 951 var addedMessage = document.getElementById(messageId); 952 if (addedMessage) { 953 addedMessage.setAttribute('data-proactive-action-id', item.id); 954 } 955 }); 956 957 if (messagesChanged && typeof chatInstance.saveConversation === 'function') { 958 chatInstance.saveConversation(); 959 } 960 961 this.pendingProactiveMessages = []; 962 this.proactiveMessageAttempts = 0; 963 }; 964
965 ListeoFloatingChatWidget.prototype.showProactiveBubble = function ( 966 actionId, 967 message, 968 priority 969 ) { 970 actionId = String(actionId || '').trim(); 971 message = String(message || '').trim(); 972 priority = Number.isFinite(priority) ? priority : 10; 973 974 if (!this.bubbleStack || !actionId || !message) return; 975 976 var bubbleId = 'listeo-floating-proactive-bubble-' + actionId; 977 if (document.getElementById(bubbleId)) return; 978 979 var bubble = document.createElement('div'); 980 bubble.className = 'listeo-floating-welcome-bubble listeo-floating-proactive-bubble'; 981 bubble.id = bubbleId; 982 bubble.setAttribute('data-proactive-action-id', actionId); 983 bubble.setAttribute('data-proactive-priority', String(priority)); 984 985 var content = document.createElement('div'); 986 content.className = 'listeo-floating-welcome-bubble-content'; 987 content.textContent = message; 988 989 var arrow = document.createElement('div'); 990 arrow.className = 'listeo-floating-welcome-bubble-arrow'; 991 992 bubble.appendChild(content); 993 bubble.appendChild(arrow); 994 995 var inserted = false; 996 var existingBubbles = this.bubbleStack.querySelectorAll( 997 '.listeo-floating-proactive-bubble' 998 ); 999 Array.prototype.some.call(existingBubbles, function (existingBubble) { 1000 var existingPriority = parseInt( 1001 existingBubble.getAttribute('data-proactive-priority'), 1002 10 1003 ); 1004 if (!Number.isFinite(existingPriority)) existingPriority = 10; 1005 1006 if (existingPriority > priority) { 1007 existingBubble.parentNode.insertBefore(bubble, existingBubble); 1008 inserted = true; 1009 return true; 1010 } 1011 return false; 1012 }); 1013 1014 if (!inserted) { 1015 this.bubbleStack.appendChild(bubble); 1016 } 1017 }; 1018
1019 ListeoFloatingChatWidget.prototype.createMiniChat = function () { 1020 var self = this; 1021 var widget = document.getElementById('listeo-floating-chat-widget'); 1022 if (!widget || this.miniChat) return; 1023 1024 var panel = document.createElement('div'); 1025 panel.className = 'listeo-floating-mini-chat'; 1026 panel.setAttribute('role', 'dialog'); 1027 panel.setAttribute('aria-live', 'polite'); 1028 1029 var identity = document.createElement('div'); 1030 identity.className = 'listeo-floating-mini-chat-identity'; 1031 identity.setAttribute('role', 'button'); 1032 identity.setAttribute('tabindex', '0'); 1033 1034 if (this.assistantAvatarUrl) { 1035 var avatar = document.createElement('img'); 1036 avatar.className = 'listeo-floating-mini-chat-avatar'; 1037 avatar.src = this.assistantAvatarUrl; 1038 avatar.alt = ''; 1039 avatar.setAttribute('aria-hidden', 'true'); 1040 identity.classList.add('has-avatar'); 1041 identity.appendChild(avatar); 1042 } 1043 1044 var assistantName = document.createElement('span'); 1045 assistantName.className = 'listeo-floating-mini-chat-assistant-name'; 1046 assistantName.textContent = this.assistantName; 1047 identity.appendChild(assistantName); 1048 1049 var closeControl = document.createElement('div'); 1050 closeControl.className = 'listeo-floating-mini-chat-close'; 1051 closeControl.setAttribute('role', 'button'); 1052 closeControl.setAttribute('tabindex', '0'); 1053 closeControl.setAttribute( 1054 'aria-label', 1055 String(this.proactiveStrings.closeMiniChat || '') 1056 ); 1057 closeControl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg>'; 1058 1059 var message = document.createElement('div'); 1060 message.className = 'listeo-floating-mini-chat-message'; 1061 1062 var quickActions = document.createElement('div'); 1063 quickActions.className = 'listeo-floating-mini-chat-quick-actions'; 1064 quickActions.hidden = true; 1065 1066 var form = document.createElement('form'); 1067 form.className = 'listeo-floating-mini-chat-form'; 1068 1069 var input = document.createElement('input'); 1070 input.type = 'text'; 1071 input.className = 'listeo-floating-mini-chat-input'; 1072 input.maxLength = 1000; 1073 input.placeholder = String(this.proactiveStrings.miniPlaceholder || ''); 1074 input.setAttribute( 1075 'aria-label', 1076 String(this.proactiveStrings.miniPlaceholder || '') 1077 ); 1078 1079 var sendButton = document.createElement('button'); 1080 sendButton.type = 'submit'; 1081 sendButton.className = 'listeo-floating-mini-chat-send'; 1082 sendButton.setAttribute( 1083 'aria-label', 1084 String(this.proactiveStrings.sendMessage || '') 1085 ); 1086 sendButton.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"></path><path d="m5 12 7-7 7 7"></path></svg>'; 1087 1088 form.appendChild(input); 1089 form.appendChild(sendButton); 1090 panel.appendChild(identity); 1091 panel.appendChild(closeControl); 1092 panel.appendChild(message); 1093 panel.appendChild(quickActions); 1094 panel.appendChild(form); 1095 widget.appendChild(panel); 1096 1097 closeControl.addEventListener('click', function () { 1098 self.dismissActiveMiniChat(); 1099 }); 1100 closeControl.addEventListener('keydown', function (event) { 1101 if (event.key !== 'Enter' && event.key !== ' ') return; 1102 event.preventDefault(); 1103 self.dismissActiveMiniChat(); 1104 }); 1105 1106 identity.addEventListener('click', function () { 1107 self.openChat(); 1108 }); 1109 identity.addEventListener('keydown', function (event) { 1110 if (event.key !== 'Enter' && event.key !== ' ') return; 1111 event.preventDefault(); 1112 self.openChat(); 1113 }); 1114 1115 quickActions.addEventListener('click', function (event) { 1116 if (!event.target || typeof event.target.closest !== 'function') return; 1117 1118 var button = event.target.closest('.listeo-floating-mini-chat-quick-action'); 1119 if (!button || !quickActions.contains(button)) return; 1120 1121 self.sendMiniChatQuestion(button.getAttribute('data-message') || ''); 1122 }); 1123 1124 form.addEventListener('submit', function (event) { 1125 event.preventDefault(); 1126 self.sendMiniChatQuestion(input.value); 1127 }); 1128 1129 this.miniChat = panel; 1130 this.miniChatMessage = message; 1131 this.miniChatQuickActions = quickActions; 1132 this.miniChatInput = input; 1133 }; 1134
1135 ListeoFloatingChatWidget.prototype.sendMiniChatQuestion = function (question) { 1136 question = String(question || '').trim().substring(0, 1000); 1137 if (!question) return; 1138 1139 this.pendingMagicQuestion = question; 1140 this.magicQuestionAttempts = 0; 1141 if (this.magicQuestionTimeoutId) { 1142 clearTimeout(this.magicQuestionTimeoutId); 1143 this.magicQuestionTimeoutId = null; 1144 } 1145 1146 this.hideMiniChat(true, true); 1147 this.openChat(); 1148 this.sendPendingMagicQuestion(); 1149 }; 1150 1151 ListeoFloatingChatWidget.prototype.renderMiniChatQuickActions = function (quickActions) { 1152 var container = this.miniChatQuickActions; 1153 if (!container) return; 1154 1155 container.textContent = ''; 1156 1157 if (!Array.isArray(quickActions)) { 1158 container.hidden = true; 1159 return; 1160 } 1161 1162 quickActions.forEach(function (quickAction) { 1163 if (!quickAction || typeof quickAction !== 'object') return; 1164 1165 var label = String(quickAction.label || '').trim(); 1166 var message = String(quickAction.message || '').trim().substring(0, 1000); 1167 if (!label || !message) return; 1168 1169 var button = document.createElement('button'); 1170 button.type = 'button'; 1171 button.className = 'listeo-floating-mini-chat-quick-action'; 1172 button.textContent = label; 1173 button.setAttribute('data-message', message); 1174 1175 var color = String(quickAction.color || '').trim(); 1176 if (/^#[0-9a-f]{3}$/i.test(color)) { 1177 color = '#' + 1178 color.charAt(1) + color.charAt(1) + 1179 color.charAt(2) + color.charAt(2) + 1180 color.charAt(3) + color.charAt(3); 1181 } 1182 if (/^#[0-9a-f]{6}$/i.test(color)) { 1183 var red = parseInt(color.substring(1, 3), 16); 1184 var green = parseInt(color.substring(3, 5), 16); 1185 var blue = parseInt(color.substring(5, 7), 16); 1186 1187 button.classList.add( 1188 'listeo-floating-mini-chat-quick-action-custom' 1189 ); 1190 button.style.setProperty('--quick-btn-color', color); 1191 button.style.setProperty( 1192 '--quick-btn-color-light', 1193 'rgba(' + red + ', ' + green + ', ' + blue + ', 0.1)' 1194 ); 1195 } 1196 1197 container.appendChild(button); 1198 }); 1199 1200 container.hidden = !container.children.length; 1201 }; 1202
1203 ListeoFloatingChatWidget.prototype.showMiniChat = function (actionId, message, quickActions) { 1204 actionId = String(actionId || '').trim(); 1205 message = String(message || '').trim(); 1206 1207 if (!actionId || !message || this.isOpen) return false; 1208 1209 this.createMiniChat(); 1210 if (!this.miniChat) return false; 1211 1212 this.miniChat.setAttribute('data-proactive-action-id', actionId); 1213 this.miniChatMessage.innerHTML = message; 1214 this.miniChat.setAttribute( 1215 'aria-label', 1216 String(this.miniChatMessage.textContent || '').trim() 1217 ); 1218 Array.prototype.forEach.call( 1219 this.miniChatMessage.querySelectorAll('a[target="_blank"]'), 1220 function (link) { 1221 var rel = String(link.getAttribute('rel') || '') 1222 .split(/\s+/) 1223 .filter(Boolean); 1224 if (rel.indexOf('noopener') === -1) rel.push('noopener'); 1225 if (rel.indexOf('noreferrer') === -1) rel.push('noreferrer'); 1226 link.setAttribute('rel', rel.join(' ')); 1227 } 1228 ); 1229 this.miniChatInput.value = ''; 1230 this.renderMiniChatQuickActions(quickActions); 1231 1232 if (this.miniChatCloseTimeoutId) { 1233 clearTimeout(this.miniChatCloseTimeoutId); 1234 this.miniChatCloseTimeoutId = null; 1235 } 1236 1237 void this.miniChat.offsetWidth; 1238 this.miniChat.classList.add('is-visible'); 1239 1240 if (this.button) { 1241 this.button.style.display = 'none'; 1242 } 1243 if (this.bubbleStack) { 1244 this.bubbleStack.style.display = 'none'; 1245 } 1246 1247 return true; 1248 }; 1249
1250 ListeoFloatingChatWidget.prototype.hideMiniChat = function (restoreLauncher, animateLauncher) { 1251 var self = this; 1252 if (!this.miniChat || !this.miniChat.classList.contains('is-visible')) return; 1253 1254 this.miniChat.classList.remove('is-visible'); 1255 if (!restoreLauncher) return; 1256 1257 if (this.miniChatCloseTimeoutId) { 1258 clearTimeout(this.miniChatCloseTimeoutId); 1259 } 1260 1261 this.miniChatCloseTimeoutId = setTimeout(function () { 1262 self.miniChatCloseTimeoutId = null; 1263 if (self.miniChat.classList.contains('is-visible')) return; 1264 1265 if (self.button) { 1266 self.button.style.display = ''; 1267 if (animateLauncher) { 1268 self.button.classList.remove('listeo-mini-chat-launcher-return'); 1269 void self.button.offsetWidth; 1270 self.button.classList.add('listeo-mini-chat-launcher-return'); 1271 setTimeout(function () { 1272 if (self.button) { 1273 self.button.classList.remove('listeo-mini-chat-launcher-return'); 1274 } 1275 }, MINI_CHAT_ANIMATION_MS); 1276 } 1277 } 1278 if (self.bubbleStack) { 1279 self.bubbleStack.style.display = ''; 1280 } 1281 }, MINI_CHAT_ANIMATION_MS); 1282 }; 1283
1284 ListeoFloatingChatWidget.prototype.lazyLoadAndInit = function () { 1285 var self = this; 1286 var chatWrapper = document.getElementById('listeo-floating-chat-instance'); 1287 var existingChatInstance = null; 1288 1289 if (chatWrapper && typeof window.jQuery !== 'undefined') { 1290 existingChatInstance = window.jQuery(chatWrapper).data('listeo-ai-chat-instance'); 1291 } 1292 1293 // shortcode on same page may have already loaded core 1294 if ( 1295 existingChatInstance || 1296 document.querySelector( 1297 'script[src*="purio-ai-scripts.js"], script[src*="ai-chat-core"]' 1298 ) 1299 ) { 1300 this.scriptsLoaded = true; 1301 this.initializeChat(); 1302 return; 1303 } 1304 1305 if (chatWrapper) { 1306 chatWrapper.classList.add('listeo-ai-chat-lazy-state'); 1307 } 1308 1309 var ver = this.scriptVersion; 1310 var urls = this.lazyScripts.map(function (url) { 1311 return url + (url.indexOf('?') === -1 ? '?' : '&') + 'ver=' + ver; 1312 }); 1313 1314 this.loadScriptsSequential(urls, 0, function () { 1315 self.scriptsLoaded = true; 1316 // let chatbot-core's ready handler settle 1317 setTimeout(function () { 1318 if (chatWrapper) { 1319 chatWrapper.classList.remove('listeo-ai-chat-lazy-state'); 1320 } 1321 self.initializeChat(); 1322 }, 50); 1323 }); 1324 }; 1325
1326 ListeoFloatingChatWidget.prototype.loadScriptsSequential = function (urls, index, callback) { 1327 var self = this; 1328 if (index >= urls.length) { 1329 callback(); 1330 return; 1331 } 1332 1333 var script = document.createElement('script'); 1334 script.src = urls[index]; 1335 script.onload = function () { 1336 self.loadScriptsSequential(urls, index + 1, callback); 1337 }; 1338 script.onerror = function () { 1339 console.error('[AI Chat] Failed to load:', urls[index]); 1340 self.loadScriptsSequential(urls, index + 1, callback); 1341 }; 1342 document.body.appendChild(script); 1343 }; 1344 1345 ListeoFloatingChatWidget.prototype.initializeChat = function () { 1346 setTimeout(function () { 1347 dispatchReady('listeo-floating-chat-instance'); 1348 }, 100); 1349 }; 1350 1351 function boot() { 1352 if (document.getElementById('listeo-floating-chat-widget')) { 1353 new ListeoFloatingChatWidget(); 1354 } 1355 } 1356 1357 // works in head (lazy mode, defer) or footer 1358 if (document.readyState === 'loading') { 1359 document.addEventListener('DOMContentLoaded', boot); 1360 } else { 1361 boot(); 1362 } 1363})();
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.