1jQuery(document).ready(function($) { 2 3 // ==================================== 4 // MULTI-INSTANCE MANAGEMENT SYSTEM 5 // ==================================== 6 7 // Instance registry - tracks all chatbot instances on the page 8 const MxChatInstances = { 9 instances: {}, 10 11 // Initialize an instance for a bot 12 init: function(botId) { 13 if (!this.instances[botId]) { 14 this.instances[botId] = { 15 botId: botId, 16 sessionId: this.getChatSession(botId), 17 lastSeenMessageId: '', 18 notificationCheckInterval: null, 19 pollingInterval: null, 20 processedMessageIds: new Set(), 21 activePdfFile: null, 22 activeWordFile: null, 23 chatHistoryLoaded: false, 24 isStreaming: false 25 }; 26 } 27 return this.instances[botId]; 28 }, 29 30 // Get instance by botId 31 get: function(botId) { 32 return this.instances[botId] || this.init(botId); 33 }, 34 35 // Get all active bot IDs 36 getAllBotIds: function() { 37 return Object.keys(this.instances); 38 }, 39 40 // Session management per bot 41 getChatSession: function(botId) { 42 var cookieName = 'mxchat_session_id_' + botId; 43 var sessionId = getCookie(cookieName); 44 45 if (!sessionId) { 46 sessionId = generateSessionId(); 47 this.setChatSession(botId, sessionId); 48 } 49 50 return sessionId; 51 }, 52 53 setChatSession: function(botId, sessionId) { 54 var cookieName = 'mxchat_session_id_' + botId; 55 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax"; 56 if (this.instances[botId]) { 57 this.instances[botId].sessionId = sessionId; 58 } 59 }, 60 61 resetChatSession: function(botId) { 62 var newSessionId = generateSessionId(); 63 this.setChatSession(botId, newSessionId); 64 var $chatBox = getElement(botId, 'chat-box'); 65 if ($chatBox.length) { 66 $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove(); 67 } 68 if (this.instances[botId]) { 69 this.instances[botId].chatHistoryLoaded = false; 70 this.instances[botId].processedMessageIds = new Set(); 71 } 72 } 73 }; 74 75 // ==================================== 76 // ELEMENT SELECTOR HELPERS 77 // ==================================== 78 79 // Check if a specific bot has an AI theme assigned (skip inline colors) 80 function shouldSkipInlineColors(botId) { 81 // If global AI theme is active, skip inline colors for all bots 82 if (mxchatChat.skip_inline_colors) { 83 return true; 84 } 85 // Check if this specific bot has a theme assignment 86 var botAssignments = mxchatChat.bot_theme_assignments || {}; 87 return botAssignments.hasOwnProperty(botId); 88 } 89 90 // Get element by ID with bot suffix - returns jQuery object 91 function getElement(botId, elementName) { 92 return $('#' + elementName + '-' + botId); 93 } 94 95 // Get element by ID with bot suffix - returns DOM element 96 function getElementDOM(botId, elementName) { 97 return document.getElementById(elementName + '-' + botId); 98 } 99 100 // Get bot ID from any element within a chatbot instance 101 function getBotIdFromElement(element) { 102 var $wrapper = $(element).closest('.mxchat-chatbot-wrapper'); 103 if ($wrapper.length) { 104 return $wrapper.data('bot-id') || 'default'; 105 } 106 // Fallback: try to find from floating container 107 var $floating = $(element).closest('.floating-chatbot'); 108 if ($floating.length) { 109 var id = $floating.attr('id') || ''; 110 var match = id.match(/floating-chatbot-(.+)/); 111 if (match) return match[1]; 112 } 113 // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id}) 114 var elementId = $(element).attr('id') || ''; 115 if (elementId) { 116 // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id} 117 var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+
117)$/); 118 if (idMatch) return idMatch[1]; 119 } 120 return 'default'; 121 } 122 123 // Get wrapper element for a bot 124 function getWrapper(botId) { 125 return getElement(botId, 'mxchat-chatbot-wrapper'); 126 } 127 128 // ==================================== 129 // GLOBAL VARIABLES & CONFIGURATION 130 // ==================================== 131 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121'; 132 133 // Initialize color settings (these are global as they come from PHP) 134 var userMessageBgColor = mxchatChat.user_message_bg_color; 135 var userMessageFontColor = mxchatChat.user_message_font_color; 136 var botMessageBgColor = mxchatChat.bot_message_bg_color; 137 var botMessageFontColor = mxchatChat.bot_message_font_color; 138 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color; 139 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color; 140 141 var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self'; 142 143 // ==================================== 144 // SESSION MANAGEMENT (Legacy compatibility) 145 // ==================================== 146 147 function getCookie(name) { 148 let value = "; " + document.cookie; 149 let parts = value.split("; " + name + "="); 150 if (parts.length == 2) return parts.pop().split(";").shift(); 151 } 152 153 function generateSessionId() { 154 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9); 155 } 156 157 // Legacy function - now delegates to instance manager 158 function getChatSession(botId) { 159 botId = botId || 'default'; 160 return MxChatInstances.getChatSession(botId); 161 } 162 163 function setChatSession(sessionId, botId) { 164 botId = botId || 'default'; 165 MxChatInstances.setChatSession(botId, sessionId); 166 } 167 168 function resetChatSession(botId) { 169 botId = botId || 'default'; 170 MxChatInstances.resetChatSession(botId); 171 } 172 173 // ==================================== 174 // INITIALIZE ALL CHATBOT INSTANCES 175 // ==================================== 176 177 function initializeAllInstances() { 178 // Find all chatbot wrappers on the page 179 $('.mxchat-chatbot-wrapper').each(function() { 180 var botId = $(this).data('bot-id') || 'default'; 181 MxChatInstances.init(botId); 182 initializeBotInstance(botId); 183 }); 184 } 185 186 function initializeBotInstance(botId) { 187 var instance = MxChatInstances.get(botId); 188 189 // Initialize quick questions state for this bot 190 checkQuickQuestionsState(botId); 191 192 // Note: Event handlers use event delegation with class selectors, 193 // so they work automatically for all instances without per-bot setup 194 } 195 196// ==================================== 197// CONTEXTUAL AWARENESS FUNCTIONALITY 198// ==================================== 199 200function getPageContext() { 201 // Check if contextual awareness is enabled 202 if (mxchatChat.contextual_awareness_toggle !== 'on') { 203 return null; 204 } 205 206 // Get page URL 207 const pageUrl = window.location.href; 208 209 // Get page title 210 const pageTitle = document.title || ''; 211 212 // Get main content from the page 213 let pageContent = ''; 214 215 // Try to get content from common content areas 216 const contentSelectors = [ 217 'main', 218 '[role="main"]', 219 '.content', 220 '.main-content', 221 '.post-content', 222 '.entry-content', 223 '.page-content', 224 'article', 225 '#content', 226 '#main' 227 ]; 228 229 let contentElement = null; 230 for (const selector of contentSelectors) { 231 contentElement = document.querySelector(selector); 232 if (contentElement) { 233 break; 234 } 235 } 236 237 // If no specific content area found, use body but exclude header, footer, nav, sidebar 238 if (!contentElement) { 239 contentElement = document.body; 240 } 241 242 if (contentElement) { 243 // Clone the element to avoid modifying the original 244 const clone = contentElement.cloneNode(true); 245 246 // Remove unwanted elements 247 const unwantedSelectors = [ 248 'header', 249 'footer', 250 'nav', 251 '.navigation', 252 '.sidebar', 253 '.widget', 254 '.menu', 255 'script', 256 'style', 257 '.comments', 258 '#comments', 259 '.breadcrumb', 260 '.breadcrumbs', 261 '#floating-chatbot', 262 '#floating-chatbot-button', 263 '.mxchat', 264 '[class*="chat"]', 265 '[id*="chat"]' 266 ]; 267
268 unwantedSelectors.forEach(selector => { 269 const elements = clone.querySelectorAll(selector); 270 elements.forEach(el => el.remove()); 271 }); 272 273 // Extract MxChat context data attributes before getting text content 274 const contextData = []; 275 clone.querySelectorAll('[data-mxchat-context]').forEach(el => { 276 const contextValue = el.dataset.mxchatContext; 277 if (contextValue && contextValue.trim()) { 278 contextData.push(contextValue); 279 } 280 }); 281 282 // Get text content and clean it up 283 pageContent = clone.textContent || clone.innerText || ''; 284 285 // Add context data to page content if any were found 286 if (contextData.length > 0) { 287 pageContent += '\n\nAdditional Context:\n' + contextData.join('\n'); 288 } 289 290 // Clean up whitespace and limit length 291 pageContent = pageContent 292 .replace(/\s+/g, ' ') 293 .trim() 294 .substring(0, 3000); // Limit to 3000 characters to avoid token limits 295 } 296 297 // Only return context if we have meaningful content 298 if (!pageContent || pageContent.length < 50) { 299 return null; 300 } 301 302 return { 303 url: pageUrl, 304 title: pageTitle, 305 content: pageContent 306 }; 307} 308 309// Track originating page when chat starts 310function trackOriginatingPage() { 311 const sessionId = getChatSession(); 312 const pageUrl = window.location.href; 313 const pageTitle = document.title || 'Untitled Page'; 314 315 // Only track once per session 316 const trackingKey = 'mxchat_originating_tracked_' + sessionId; 317 if (sessionStorage.getItem(trackingKey)) { 318 return; 319 } 320 321 $.ajax({
322 url: mxchatChat.ajax_url, 323 type: 'POST', 324 data: { 325 action: 'mxchat_track_originating_page', 326 session_id: sessionId, 327 page_url: pageUrl, 328 page_title: pageTitle, 329 nonce: mxchatChat.nonce 330 }, 331 success: function(response) { 332 if (response.success) { 333 sessionStorage.setItem(trackingKey, 'true'); 334 } 335 } 336 }); 337} 338 339// ==================================== 340// CORE CHAT FUNCTIONALITY 341// ==================================== 342 343// Helper functions to disable/enable chat input while waiting for response 344function disableChatInput(botId) { 345 botId = botId || 'default'; 346 var chatInput = getElementDOM(botId, 'chat-input'); 347 var sendButton = getElementDOM(botId, 'send-button'); 348 if (chatInput) { 349 chatInput.disabled = true; 350 chatInput.style.opacity = '0.6'; 351 } 352 if (sendButton) { 353 sendButton.disabled = true; 354 sendButton.style.opacity = '0.5'; 355 sendButton.style.pointerEvents = 'none'; 356 } 357} 358 359function enableChatInput(botId) { 360 botId = botId || 'default'; 361 var chatInput = getElementDOM(botId, 'chat-input'); 362 var sendButton = getElementDOM(botId, 'send-button'); 363 if (chatInput) { 364 chatInput.disabled = false; 365 chatInput.style.opacity = '1'; 366 chatInput.focus(); 367 } 368 if (sendButton) { 369 sendButton.disabled = false; 370 sendButton.style.opacity = '1'; 371 sendButton.style.pointerEvents = 'auto'; 372 } 373} 374 375// Update your existing sendMessage function 376function sendMessage(botId) { 377 botId = botId || 'default'; 378 var $chatInput = getElement(botId, 'chat-input'); 379 var message = $chatInput.val(); 380 381 // ADD PROMPT HOOK HERE 382 if (typeof customMxChatFilter === 'function') { 383 message = customMxChatFilter(message, "prompt"); 384 } 385 386 if (message) { 387 // Disable input while waiting for response 388 disableChatInput(botId); 389 390 appendMessage("user", message, '', [], false, botId); 391 $chatInput.val(''); 392 $chatInput.css('height', 'auto'); 393 394 if (hasQuickQuestions(botId)) { 395 collapseQuickQuestions(botId); 396 } 397 appendThinkingMessage(botId); 398 scrollToBottom(botId); 399 400 const currentModel = mxchatChat.model || 'gpt-4o'; 401 402 // Check if streaming is enabled AND supported for this model 403 if (shouldUseStreaming(currentModel)) { 404 callMxChatStream(message, function(response) { 405 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message'); 406 }, botId); 407 } else { 408 callMxChat(message, function(response) { 409 replaceLastMessage("bot", response, '', [], botId); 410 }, botId); 411 } 412 } 413} 414 415// Update your existing sendMessageToChatbot function 416function sendMessageToChatbot(message, botId) { 417 botId = botId || 'default'; 418 419 // ADD PROMPT HOOK HERE 420 if (typeof customMxChatFilter === 'function') { 421 message = customMxChatFilter(message, "prompt"); 422 } 423 424 // Disable input while waiting for response 425 disableChatInput(botId); 426 427 var sessionId = getChatSession(botId); 428 429 if (hasQuickQuestions(botId)) { 430 collapseQuickQuestions(botId); 431 } 432 appendThinkingMessage(botId); 433 scrollToBottom(botId); 434 435 const currentModel = mxchatChat.model || 'gpt-4o'; 436 437 // Check if streaming is enabled AND supported for this model 438 if (shouldUseStreaming(currentModel)) { 439 callMxChatStream(message, function(response) { 440 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message'); 441 }, botId); 442 } else { 443 callMxChat(message, function(response) { 444 getElement(botId, 'chat-box').find('.temporary-message').remove(); 445 replaceLastMessage("bot", response, '', [], botId); 446 }, botId); 447 } 448} 449 450// Updated shouldUseStreaming function with debugging 451function shouldUseStreaming(model) { 452 // Check if streaming is enabled in settings (using your toggle naming pattern) 453 const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on'; 454 455 // Check if model supports streaming 456 const streamingSupported = isStreamingSupported(model); 457 458 459 // Only use streaming if both enabled and supported 460 return streamingEnabled && streamingSupported; 461} 462 463// Helper function to handle chat mode updates 464function handleChatModeUpdates(response, responseText) {
465 // Check for explicit chat mode in response (THIS IS THE KEY FIX) 466 if (response.chat_mode) { 467 updateChatModeIndicator(response.chat_mode); 468 return; // Return early since we found explicit mode 469 } 470 // Check for fallback response chat mode 471 else if (response.fallbackResponse && response.fallbackResponse.chat_mode) { 472 updateChatModeIndicator(response.fallbackResponse.chat_mode); 473 return; // Return early since we found explicit mode 474 } 475 476 // Only do text-based detection if no explicit mode was provided 477 // Check for specific AI chatbot response text 478 if (responseText === 'You are now chatting with the AI chatbot.' || 479 responseText.includes('now chatting with the AI') || 480 responseText.includes('switched to AI mode') || 481 responseText.includes('AI chatbot is now')) { 482 updateChatModeIndicator('ai'); 483 } 484 // Check for agent transfer messages 485 else if (responseText.includes('agent') && 486 (responseText.includes('transfer') || responseText.includes('connected'))) { 487 updateChatModeIndicator('agent'); 488 } 489} 490 491// Function to get bot ID from any element or wrapper 492// If element is provided, finds the bot ID from its wrapper 493// If no element, returns 'default' (for backward compatibility) 494function getMxChatBotId(element) { 495 if (element) { 496 return getBotIdFromElement(element); 497 } 498 // Fallback: find first chatbot wrapper on page 499 const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper'); 500 return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default'; 501} 502 503function callMxChat(message, callback, botId) { 504 botId = botId || getMxChatBotId(); 505 506 // Store the message in case we need to retry after session reset 507 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message); 508 509 // Get page context if contextual awareness is enabled 510 const pageContext = getPageContext(); 511 512 // Prepare AJAX data 513 const ajaxData = { 514 action: 'mxchat_handle_chat_request', 515 message: message, 516 session_id: getChatSession(botId), 517 nonce: mxchatChat.nonce, 518 current_page_url: window.location.href, 519 current_page_title: document.title, 520 bot_id: botId 521 }; 522 523 // Add page context if available 524 if (pageContext) { 525 ajaxData.page_context = JSON.stringify(pageContext); 526 } 527 528 // CHECK FOR VISION FLAGS AND ADD THEM 529 if (window.mxchatVisionProcessed) { 530 ajaxData.vision_processed = true; 531 ajaxData.original_user_message = window.mxchatOriginalMessage || message; 532 ajaxData.vision_images_count = window.mxchatVisionImagesCount || 0; 533 // Clear the flags after use 534 window.mxchatVisionProcessed = false; 535 window.mxchatOriginalMessage = null; 536 window.mxchatVisionImagesCount = 0; 537 } 538 539 $.ajax({
540 url: mxchatChat.ajax_url, 541 type: 'POST', 542 dataType: 'json', 543 data: ajaxData, 544 success: function(response) { 545 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST 546 if (response.chat_mode) { 547 updateChatModeIndicator(response.chat_mode, botId); 548 } 549 550 // Also check in data property if response is wrapped 551 if (response.data && response.data.chat_mode) { 552 updateChatModeIndicator(response.data.chat_mode, botId); 553 } 554 555 // SECURITY FIX: Check for errors FIRST before checking for success 556 // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed 557 if (response.success === false || (response.data && response.data.error_message)) { 558 let errorMessage = ""; 559 let errorCode = ""; 560 561 // Check various possible error locations in the response 562 if (response.data && response.data.error_message) { 563 errorMessage = response.data.error_message; 564 errorCode = response.data.error_code || ""; 565 } else if (response.error_message) { 566 errorMessage = response.error_message; 567 errorCode = response.error_code || ""; 568 } else if (response.message) { 569 errorMessage = response.message; 570 } else if (typeof response.data === 'string') { 571 errorMessage = response.data; 572 } else { 573 // Fallback for any other unexpected response format 574 errorMessage = "An error occurred. Please try again or contact support."; 575 } 576 577 // Handle session reset action (IP changed, session expired, etc.) 578 if (response.data && response.data.action === 'reset_session') { 579 // Clear the old session and generate a new one 580 resetChatSession(botId); 581 // Remove the temporary loading message 582 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 583 // Re-send the original message with the new session 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message'); 585 if (originalMessage) { 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null); 587 // Re-add the user message and thinking indicator 588 appendMessage("user", originalMessage, '', [], false, botId); 589 appendThinkingMessage(botId); 590 scrollToBottom(botId); 591 // Determine whether to use streaming 592 const currentModel = mxchatChat.model || 'gpt-4o'; 593 if (shouldUseStreaming(currentModel)) { 594 callMxChatStream(originalMessage, function(response) { 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message'); 596 }, botId); 597 } else { 598 callMxChat(originalMessage, function(response) { 599 replaceLastMessage("bot", response, '', [], botId); 600 }, botId); 601 } 602 } 603 return; 604 } 605 606 // Format user-friendly error message 607 let displayMessage = errorMessage; 608 609 // Customize message for admin users 610 if (mxchatChat.is_admin) { 611 // For admin users, show more technical details including error code 612 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : ""); 613 } 614 615 replaceLastMessage("bot", displayMessage, '', [], botId); 616 return; // Exit early for errors 617 } 618 619 // NOW check if this is a successful response by looking for text, html, or message fields 620 // This preserves compatibility with your server response format 621 if (response.text !== undefined || response.html !== undefined || response.message !== undefined || 622 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) { 623 624 // Handle successful response - this is your original success handling code 625 626 // Handle other responses 627 let responseText = response.text || ''; 628 let responseHtml = response.html || ''; 629 let responseMessage = response.message || ''; 630 631 // Add PDF filename handling 632 if (response.data && response.data.filename) { 633 showActivePdf(response.data.filename, botId);
634 var instance = MxChatInstances.get(botId); 635 instance.activePdfFile = response.data.filename; 636 } 637 638 // Add redirect check here 639 if (response.redirect_url) { 640 if (responseText) { 641 replaceLastMessage("bot", responseText, '', [], botId); 642 } 643 setTimeout(() => { 644 window.location.href = response.redirect_url; 645 }, 1500); 646 return; 647 } 648 649 // Check for live agent response 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') { 651 updateChatModeIndicator('agent', botId); 652 return; 653 } 654 655 // Handle the message and show notification if chat is hidden 656 if (responseText || responseHtml || responseMessage) { 657 658 // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING 659 if (responseText && typeof customMxChatFilter === 'function') { 660 responseText = customMxChatFilter(responseText, "response"); 661 } 662 if (responseMessage && typeof customMxChatFilter === 'function') { 663 responseMessage = customMxChatFilter(responseMessage, "response"); 664 } 665 666 // Update the messages as before 667 if (responseText && responseHtml) { 668 replaceLastMessage("bot", responseText, responseHtml, [], botId); 669 } else if (responseText) { 670 replaceLastMessage("bot", responseText, '', [], botId); 671 } else if (responseHtml) { 672 replaceLastMessage("bot", "", responseHtml, [], botId); 673 } else if (responseMessage) { 674 replaceLastMessage("bot", responseMessage, '', [], botId); 675 } 676 677 // Check if chat is hidden and show notification 678 var $floatingChatbot = getElement(botId, 'floating-chatbot'); 679 if ($floatingChatbot.hasClass('hidden')) { 680 var $badge = getElement(botId, 'chat-notification-badge'); 681 if ($badge.length) { 682 $badge.show(); 683 } 684 } 685 } else { 686 replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId); 687 } 688 689 if (response.message_id) { 690 var instance = MxChatInstances.get(botId); 691 instance.lastSeenMessageId = response.message_id; 692 } 693 694 return; 695 } 696 697 // Fallback for truly unexpected response formats 698 replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId); 699 }, 700 error: function(xhr, status, error) { 701 let errorMessage = "An unexpected error occurred."; 702 703 // Try to parse the response if it's JSON 704 try { 705 const responseJson = JSON.parse(xhr.responseText); 706 707 if (responseJson.data && responseJson.data.error_message) { 708 errorMessage = responseJson.data.error_message; 709 } else if (responseJson.message) { 710 errorMessage = responseJson.message; 711 } 712 } catch (e) { 713 // Not JSON or parsing failed, use HTTP status based messages 714 if (xhr.status === 0) { 715 errorMessage = "Network error: Please check your internet connection."; 716 } else if (xhr.status === 403) { 717 errorMessage = "Access denied: Your session may have expired. Please refresh the page."; 718 } else if (xhr.status === 404) { 719 errorMessage = "API endpoint not found. Please contact support."; 720 } else if (xhr.status === 429) { 721 errorMessage = "Too many requests. Please try again in a moment."; 722 } else if (xhr.status >= 500) { 723 errorMessage = "Server error: The server encountered an issue. Please try again later."; 724 } 725 } 726 727 replaceLastMessage("bot", errorMessage, '', [], botId); 728 } 729 });
730} 731 732function callMxChatStream(message, callback, botId) { 733 botId = botId || getMxChatBotId(); 734 735 // Store the message in case we need to retry after session reset 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message); 737 738 const currentModel = mxchatChat.model || 'gpt-4o'; 739 if (!isStreamingSupported(currentModel)) { 740 callMxChat(message, callback, botId); 741 return; 742 } 743 744 // Get page context if contextual awareness is enabled 745 const pageContext = getPageContext(); 746 747 const formData = new FormData(); 748 formData.append('action', 'mxchat_stream_chat'); 749 formData.append('message', message); 750 formData.append('session_id', getChatSession(botId)); 751 formData.append('nonce', mxchatChat.nonce); 752 formData.append('current_page_url', window.location.href); 753 formData.append('current_page_title', document.title); 754 formData.append('bot_id', botId); 755 756 // Add page context if available 757 if (pageContext) { 758 formData.append('page_context', JSON.stringify(pageContext)); 759 } 760 761 // CHECK FOR VISION FLAGS AND ADD THEM 762 if (window.mxchatVisionProcessed) { 763 formData.append('vision_processed', 'true'); 764 formData.append('original_user_message', window.mxchatOriginalMessage || message); 765 formData.append('vision_images_count', window.mxchatVisionImagesCount || '0'); 766 // Clear the flags after use 767 window.mxchatVisionProcessed = false; 768 window.mxchatOriginalMessage = null; 769 window.mxchatVisionImagesCount = 0; 770 } 771 772 let accumulatedContent = ''; 773 let testingDataReceived = false; 774 let streamingStarted = false; 775 776 fetch(mxchatChat.ajax_url, { 777 method: 'POST', 778 body: formData, 779 credentials: 'same-origin' 780 }) 781 .then(response => { 782 // Store the response for potential fallback handling 783 const responseClone = response.clone(); 784 785 if (!response.ok) { 786 // Try to get error details from response 787 return responseClone.json().then(errorData => { 788 throw { isServerError: true, data: errorData }; 789 }).catch(() => { 790 throw new Error('Network response was not ok'); 791 }); 792 } 793 794 // Check if response is JSON instead of streaming 795 const contentType = response.headers.get('content-type'); 796 if (contentType && contentType.includes('application/json')) { 797 return responseClone.json().then(data => { 798 // IMMEDIATE CHAT MODE UPDATE for JSON response 799 if (data.chat_mode) { 800 updateChatModeIndicator(data.chat_mode, botId); 801 } 802 803 // Check for testing panel 804 if (window.mxchatTestPanelInstance && data.testing_data) { 805 window.mxchatTestPanelInstance.handleTestingData(data.testing_data); 806 } 807 808 // Handle the JSON response directly 809 handleNonStreamResponse(data, callback, botId); 810 return Promise.resolve(); // Prevent further processing 811 }); 812 } 813 814 // Continue with streaming processing 815 const reader = response.body.getReader(); 816 const decoder = new TextDecoder(); 817 let buffer = ''; 818 819 function processStream() { 820 reader.read().then(({ done, value }) => { 821 if (done) { 822 // If streaming completed but no content was received, try to get response as fallback 823 if (!streamingStarted || !accumulatedContent) { 824 // Try to read the response as JSON 825 responseClone.text().then(text => { 826 try { 827 const data = JSON.parse(text); 828 if (data.text || data.message || data.html) { 829 handleNonStreamResponse(data, callback, botId); 830 } else { 831 // No valid data, fall back to regular call 832 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 833 callMxChat(message, callback, botId); 834 } 835 } catch (e) { 836 // Could not parse, fall back to regular call 837 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 838 callMxChat(message, callback, botId); 839 } 840 }).catch(() => { 841 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 842 callMxChat(message, callback, botId); 843 }); 844 return; 845 } 846 847 // Re-enable chat input when stream ends with content 848 enableChatInput(botId); 849 850 if (callback) { 851 callback(accumulatedContent); 852 } 853 return; 854 } 855 856 buffer += decoder.decode(value, { stream: true });
857 const lines = buffer.split('\n'); 858 buffer = lines.pop() || ''; 859 860 for (const line of lines) { 861 if (line.startsWith('data: ')) { 862 const data = line.substring(6); 863 864 if (data === '[DONE]') { 865 if (!accumulatedContent) { 866 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 867 callMxChat(message, callback, botId); 868 return; 869 } 870 871 // Re-enable chat input after streaming completes 872 enableChatInput(botId); 873 874 if (callback) { 875 callback(accumulatedContent); 876 } 877 return; 878 } 879 880 try { 881 const json = JSON.parse(data); 882 883 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING 884 if (json.chat_mode) { 885 updateChatModeIndicator(json.chat_mode, botId); 886 } 887 888 // Handle testing data 889 if (json.testing_data && !testingDataReceived) { 890 if (window.mxchatTestPanelInstance) { 891 window.mxchatTestPanelInstance.handleTestingData(json.testing_data); 892 testingDataReceived = true; 893 } 894 } 895 // Handle content streaming 896 else if (json.content) { 897 streamingStarted = true; 898 accumulatedContent += json.content;
899 updateStreamingMessage(accumulatedContent, botId); 900 } 901 // Handle complete response in stream (fallback response) 902 else if (json.text || json.message || json.html) { 903 handleNonStreamResponse(json, callback, botId); 904 return; 905 } 906 // Handle errors 907 else if (json.error) { 908 909 // Get error message from various possible fields 910 let errorMessage = json.error_message || json.message || json.text || 911 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.'); 912 913 // Re-enable chat input on error 914 enableChatInput(botId); 915 916 // Display the error directly in the chat 917 replaceLastMessage("bot", errorMessage, '', [], botId); 918 919 if (callback) { 920 callback(errorMessage); 921 } 922 return; 923 } 924 } catch (e) { 925 // SSE data parsing error - silently continue 926 } 927 } 928 } 929 930 processStream(); 931 }).catch(streamError => { 932 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 933 callMxChat(message, callback, botId); 934 }); 935 } 936 937 processStream(); 938 }) 939 .catch(error => { 940 // Check if we have server error data with chat mode 941 if (error && error.isServerError && error.data) { 942 // Check for chat mode in error data 943 if (error.data.chat_mode) { 944 updateChatModeIndicator(error.data.chat_mode, botId); 945 } 946 947 handleNonStreamResponse(error.data, callback, botId); 948 } else { 949 // Only fall back to regular call if we don't have any response data 950 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove(); 951 callMxChat(message, callback, botId); 952 } 953 }); 954} 955 956// Helper function to handle non-streaming responses 957function handleNonStreamResponse(data, callback, botId) { 958 botId = botId || 'default'; 959 960 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES 961 if (data.chat_mode) { 962 updateChatModeIndicator(data.chat_mode, botId); 963 } 964 965 // Also check in data property if response is wrapped 966 if (data.data && data.data.chat_mode) { 967 updateChatModeIndicator(data.data.chat_mode, botId); 968 } 969 970 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it 971 // This prevents a visual gap between thinking dots disappearing and content appearing 972 973 // SECURITY FIX: Check for errors FIRST 974 if (data.success === false || (data.data && data.data.error_message)) { 975 let errorMessage = ""; 976 let errorCode = ""; 977 978 // Check various possible error locations 979 if (data.data && data.data.error_message) { 980 errorMessage = data.data.error_message; 981 errorCode = data.data.error_code || ""; 982 } else if (data.error_message) { 983 errorMessage = data.error_message; 984 errorCode = data.error_code || ""; 985 } else if (data.message) { 986 errorMessage = data.message; 987 } else if (typeof data.data === 'string') { 988 errorMessage = data.data; 989 } else { 990 errorMessage = "An error occurred. Please try again or contact support."; 991 } 992 993 // Handle session reset action (IP changed, session expired, etc.) 994 if (data.data && data.data.action === 'reset_session') { 995 // Clear the old session and generate a new one 996 resetChatSession(botId); 997 // Re-send the original message with the new session 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message'); 999 if (originalMessage) { 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null); 1001 // Re-add the user message and thinking indicator 1002 appendMessage("user", originalMessage, '', [], false, botId); 1003 appendThinkingMessage(botId); 1004 scrollToBottom(botId); 1005 // Determine whether to use streaming 1006 const currentModel = mxchatChat.model || 'gpt-4o'; 1007 if (shouldUseStreaming(currentModel)) { 1008 callMxChatStream(originalMessage, callback, botId); 1009 } else { 1010 callMxChat(originalMessage, callback, botId); 1011 } 1012 } 1013 return; 1014 } 1015 1016 // Format user-friendly error message 1017 let displayMessage = errorMessage; 1018 if (mxchatChat.is_admin) { 1019 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : ""); 1020 } 1021 1022 replaceLastMessage("bot", displayMessage, '', [], botId); 1023 1024 if (callback) { 1025 callback(''); 1026 } 1027 return; // Exit early for errors 1028 } 1029 1030 // Handle different response formats 1031 if (data.text || data.html || data.message) { 1032 1033 // Apply response hooks 1034 if (data.text && typeof customMxChatFilter === 'function') { 1035 data.text = customMxChatFilter(data.text, "response"); 1036 } 1037 if (data.message && typeof customMxChatFilter === 'function') { 1038 data.message = customMxChatFilter(data.message, "response"); 1039 } 1040 1041 // Display the response 1042 if (data.text && data.html) { 1043 replaceLastMessage("bot", data.text, data.html, [], botId); 1044 } else if (data.text) { 1045 replaceLastMessage("bot", data.text, '', [], botId); 1046 } else if (data.html) { 1047 replaceLastMessage("bot", "", data.html, [], botId); 1048 } else if (data.message) { 1049 replaceLastMessage("bot", data.message, '', [], botId); 1050 } 1051 } 1052 1053 // Handle other response properties 1054 if (data.data && data.data.filename) { 1055 showActivePdf(data.data.filename, botId);
1056 var instance = MxChatInstances.get(botId); 1057 instance.activePdfFile = data.data.filename; 1058 } 1059 1060 if (data.redirect_url) { 1061 setTimeout(() => { 1062 window.location.href = data.redirect_url; 1063 }, 1500); 1064 } 1065 1066 // Ensure chat input is re-enabled (safety net for edge cases) 1067 enableChatInput(botId); 1068 1069 if (callback) { 1070 callback(data.text || data.message || ''); 1071 } 1072} 1073 1074// Enhanced updateChatModeIndicator function for immediate DOM updates 1075function updateChatModeIndicator(mode, botId) { 1076 console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId); 1077 botId = botId || 'default'; 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator'); 1079 console.log('[MxChat] chat-mode-indicator element found:', !!indicator); 1080 if (indicator) { 1081 const oldText = indicator.textContent; 1082 console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode); 1083 1084 if (mode === 'agent') { 1085 indicator.textContent = 'Live Agent'; 1086 console.log('[MxChat] Mode is agent, calling startPolling...'); 1087 startPolling(botId); 1088 } else { 1089 // Everything else is AI mode 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent'; 1091 indicator.textContent = customAiText; 1092 stopPolling(botId); 1093 } 1094 1095 // Force immediate DOM update and reflow 1096 if (oldText !== indicator.textContent) { 1097 // Force a reflow to ensure the change is visible immediately 1098 indicator.style.display = 'none'; 1099 indicator.offsetHeight; // Trigger reflow 1100 indicator.style.display = ''; 1101 1102 // Double-check after a brief moment to ensure the change stuck 1103 setTimeout(() => { 1104 if (mode === 'agent' && indicator.textContent !== 'Live Agent') { 1105 indicator.textContent = 'Live Agent'; 1106 } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') { 1107 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent'; 1108 indicator.textContent = customAiText; 1109 } 1110 }, 50); 1111 } 1112 } 1113} 1114 1115// Function to update message during streaming 1116function updateStreamingMessage(content, botId) { 1117 botId = botId || 'default'; 1118 1119 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING 1120 if (typeof customMxChatFilter === 'function') { 1121 content = customMxChatFilter(content, "response"); 1122 } 1123 1124 const formattedContent = linkify(content); 1125 1126 // Find the temporary message in this bot's chat box 1127 var $chatBox = getElement(botId, 'chat-box'); 1128 const tempMessage = $chatBox.find('.bot-message.temporary-message').last(); 1129 1130 if (tempMessage.length) { 1131 // Update existing message 1132 tempMessage.html(formattedContent); 1133 } else { 1134 // Create new temporary message if it doesn't exist 1135 appendMessage("bot", content, '', [], true, botId); 1136 } 1137} 1138 1139function isStreamingSupported(model) { 1140 if (!model) return false; 1141 1142 const modelPrefix = model.split('-')[0].toLowerCase(); 1143 1144 // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models 1145 const isSupported = modelPrefix === 'gpt' || 1146 modelPrefix === 'o1' || 1147 modelPrefix === 'claude' || 1148 modelPrefix === 'grok' || 1149 modelPrefix === 'deepseek' || 1150 model === 'openrouter'; // Add this line - check full model name for OpenRouter 1151 1152 return isSupported; 1153} 1154 1155// Update the event handlers to use the correct function names (using event delegation) 1156// Use class-based selectors for multi-instance support 1157$(document).on('click', '.send-button', function() { 1158 var botId = getBotIdFromElement(this); 1159 disableChatInput(botId); 1160 sendMessage(botId); 1161}); 1162 1163// Override enter key handler (using event delegation) 1164$(document).on('keypress', '.chat-input', function(e) { 1165 if (e.which == 13 && !e.shiftKey) { 1166 e.preventDefault(); 1167 var botId = getBotIdFromElement(this); 1168 disableChatInput(botId); 1169 sendMessage(botId); 1170 } 1171}); 1172 1173 1174function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') { 1175 try {
1176 // Determine styles based on sender type 1177 let messageClass, bgColor, fontColor; 1178 1179 if (sender === "user") { 1180 messageClass = "user-message"; 1181 bgColor = userMessageBgColor; 1182 fontColor = userMessageFontColor; 1183 // Only sanitize user input 1184 messageText = sanitizeUserInput(messageText); 1185 } else if (sender === "agent") { 1186 messageClass = "agent-message"; 1187 bgColor = liveAgentMessageBgColor; 1188 fontColor = liveAgentMessageFontColor; 1189 } else { 1190 messageClass = "bot-message"; 1191 bgColor = botMessageBgColor; 1192 fontColor = botMessageFontColor; 1193 } 1194 1195 const messageDiv = $('<div>') 1196 .addClass(messageClass) 1197 .attr('dir', 'auto'); 1198 1199 // Only apply inline colors if AI theme is not active (let CSS handle it) 1200 var skipColors = shouldSkipInlineColors(botId); 1201 if (skipColors) { 1202 messageDiv.css({ 1203 'margin-bottom': '1em' 1204 }); 1205 } else { 1206 messageDiv.css({ 1207 'background': bgColor, 1208 'color': fontColor, 1209 'margin-bottom': '1em' 1210 }); 1211 } 1212 1213 // Process the message content based on sender 1214 let fullMessage; 1215 if (sender === "user") { 1216 // For user messages, apply linkify after sanitization 1217 fullMessage = linkify(messageText); 1218 } else { 1219 // For bot/agent messages, preserve HTML 1220 fullMessage = messageText; 1221 } 1222 1223 // Add images if provided 1224 if (images && images.length > 0) { 1225 fullMessage += '<div class="image-gallery" dir="auto">'; 1226 images.forEach(img => { 1227 const safeTitle = sanitizeUserInput(img.title); 1228 const safeUrl = encodeURI(img.image_url); 1229 const safeThumbnail = encodeURI(img.thumbnail_url); 1230 1231 fullMessage += ` 1232 <div style="margin-bottom: 10px;"> 1233 <strong>${safeTitle}</strong><br> 1234 <a href="${safeUrl}" target="_blank"> 1235 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" /> 1236 </a> 1237 </div>`; 1238 }); 1239 fullMessage += '</div>'; 1240 } 1241 1242 // Append HTML content if provided 1243 if (messageHtml && sender !== "user") { 1244 // Only add line breaks if there's actual text content before the HTML 1245 if (fullMessage && fullMessage.trim()) { 1246 fullMessage += '<br><br>' + messageHtml; 1247 } else { 1248 fullMessage = messageHtml; 1249 } 1250 } 1251 1252 messageDiv.html(fullMessage); 1253 1254 if (isTemporary) { 1255 messageDiv.addClass('temporary-message'); 1256 } 1257 1258 // Append to the correct chatbot instance's chat-box 1259 var $chatBox = getElement(botId, 'chat-box'); 1260 messageDiv.hide().appendTo($chatBox).fadeIn(300, function() { 1261 // FIXED: Use event delegation for link tracking 1262 if (sender === "bot" || sender === "agent") { 1263 attachLinkTracking(messageDiv, messageText, botId); 1264 } 1265 1266 if (sender === "bot") { 1267 const lastUserMessage = $chatBox.find('.user-message').last(); 1268 if (lastUserMessage.length) { 1269 scrollElementToTop(lastUserMessage, botId); 1270 } 1271 } 1272 }); 1273 1274 if (messageText.id) { 1275 var instance = MxChatInstances.get(botId); 1276 instance.lastSeenMessageId = messageText.id; 1277 hideNotification(botId); 1278 } 1279 } catch (error) { 1280 // Error rendering message - silently continue 1281 } 1282} 1283 1284// Helper function to attach link tracking with proper event handling 1285function attachLinkTracking(messageDiv, messageText, botId) { 1286 botId = botId || 'default'; 1287 // Use a slight delay to ensure DOM is ready 1288 setTimeout(function() { 1289 const links = messageDiv.find('a[href]').not('[data-tracked]'); 1290 1291 links.each(function() { 1292 const $link = $(this); 1293 const originalHref = $link.attr('href'); 1294 1295 // Mark as tracked to avoid duplicate handlers 1296 $link.attr('data-tracked', 'true'); 1297 1298 // Only track external URLs 1299 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) { 1300 // Remove any existing click handlers first 1301 $link.off('click.tracking'); 1302 1303 // Add new click handler with namespace 1304 $link.on('click.tracking', function(e) { 1305 e.preventDefault(); 1306 e.stopPropagation(); 1307 1308 const messageContext = typeof messageText === 'string' 1309 ? messageText.substring(0, 200) 1310 : ''; 1311 1312 // Track the click 1313 $.ajax({
1314 url: mxchatChat.ajax_url, 1315 type: 'POST', 1316 data: { 1317 action: 'mxchat_track_url_click', 1318 session_id: getChatSession(botId), 1319 url: originalHref, 1320 message_context: messageContext, 1321 nonce: mxchatChat.nonce 1322 }, 1323 complete: function() { 1324 // Always redirect, even if tracking fails 1325 if ($link.attr('target') === '_blank' || linkTarget === '_blank') { 1326 window.open(originalHref, '_blank'); 1327 } else { 1328 window.location.href = originalHref; 1329 } 1330 } 1331 }); 1332 1333 return false; // Extra insurance to prevent default 1334 }); 1335 } 1336 }); 1337 }, 100); // Small delay to ensure DOM is ready 1338} 1339 1340function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') { 1341 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message"; 1342 var $chatBox = getElement(botId, 'chat-box'); 1343 var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last(); 1344 1345 // Determine styles 1346 let bgColor, fontColor; 1347 if (sender === "user") { 1348 bgColor = userMessageBgColor; 1349 fontColor = userMessageFontColor; 1350 } else if (sender === "agent") { 1351 bgColor = liveAgentMessageBgColor; 1352 fontColor = liveAgentMessageFontColor; 1353 } else { 1354 bgColor = botMessageBgColor; 1355 fontColor = botMessageFontColor; 1356 } 1357 1358 // FIXED: Only linkify if response doesn't already contain HTML links or tags 1359 // This prevents double-processing of URLs that are already formatted as HTML 1360 var fullMessage; 1361 if (sender === "user") { 1362 // Always linkify user messages (they're plain text) 1363 fullMessage = linkify(responseText); 1364 } else { 1365 // For bot/agent messages, check if HTML already exists 1366 if (responseText.includes('<a href=') || responseText.includes('</a>') || 1367 responseText.includes('<img') || responseText.includes('<div') || 1368 responseText.includes('<p>') || responseText.includes('<br>')) { 1369 // Response already has HTML, don't process it 1370 fullMessage = responseText; 1371 } else { 1372 // Plain text response, apply linkify 1373 fullMessage = linkify(responseText); 1374 } 1375 } 1376 1377 if (responseHtml) { 1378 // Only add line breaks if there's actual text content before the HTML 1379 if (fullMessage && fullMessage.trim()) { 1380 fullMessage += '<br><br>' + responseHtml; 1381 } else { 1382 fullMessage = responseHtml; 1383 } 1384 } 1385 1386 if (images.length > 0) { 1387 fullMessage += '<div class="image-gallery" dir="auto">'; 1388 images.forEach(img => { 1389 fullMessage += ` 1390 <div style="margin-bottom: 10px;"> 1391 <strong>${img.title}</strong><br> 1392 <a href="${img.image_url}" target="_blank"> 1393 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" /> 1394 </a> 1395 </div>`; 1396 }); 1397 fullMessage += '</div>'; 1398 } 1399 1400 if (lastMessageDiv.length) { 1401 // Replace content immediately to prevent visual gap between thinking dots and response 1402 lastMessageDiv 1403 .html(fullMessage) 1404 .removeClass('bot-message user-message temporary-message') 1405 .addClass(messageClass) 1406 .attr('dir', 'auto'); 1407 1408 // Only apply inline colors if AI theme is not active (let CSS handle it) 1409 var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId); 1410 if (!skipColors) { 1411 lastMessageDiv.css({ 1412 'background-color': bgColor, 1413 'color': fontColor, 1414 }); 1415 } 1416 1417 // Handle link tracking and scroll 1418 if (sender === "bot" || sender === "agent") { 1419 attachLinkTracking(lastMessageDiv, responseText, botId); 1420 1421 const lastUserMessage = $chatBox.find('.user-message').last(); 1422 if (lastUserMessage.length) { 1423 scrollElementToTop(lastUserMessage, botId); 1424 } 1425 // Show notification if chat is hidden 1426 var $floatingChatbot = getElement(botId, 'floating-chatbot'); 1427 if ($floatingChatbot.hasClass('hidden')) { 1428 showNotification(botId); 1429 } 1430 } 1431 1432 // Re-enable chat input after response is displayed 1433 enableChatInput(botId); 1434 } else { 1435 appendMessage(sender, responseText, responseHtml, images, false, botId); 1436 // Re-enable chat input after response is displayed 1437 enableChatInput(botId); 1438 } 1439} 1440 1441 1442 function appendThinkingMessage(botId) { 1443 botId = botId || 'default'; 1444 var $chatBox = getElement(botId, 'chat-box'); 1445 1446 // Remove any existing thinking dots in this bot's chat first 1447 $chatBox.find('.thinking-dots').remove(); 1448 1449 // Check if we should skip inline colors (AI theme is active) 1450 var skipColors = shouldSkipInlineColors(botId); 1451 1452 // Retrieve the bot message font color and background color 1453 var botMessageFontColor = mxchatChat.bot_message_font_color; 1454 var botMessageBgColor = mxchatChat.bot_message_bg_color; 1455 1456 // Build thinking dots HTML - skip inline colors if AI theme is active 1457 var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"'; 1458 var thinkingHtml = '<div class="thinking-dots-container">' + 1459 '<div class="thinking-dots">' + 1460 '<span class="dot"' + dotStyle + '></span>' + 1461 '<span class="dot"' + dotStyle + '></span>' + 1462 '<span class="dot"' + dotStyle + '></span>' + 1463 '</div>' + 1464 '</div>'; 1465 1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active 1467 var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"'; 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>'); 1469 scrollToBottom(botId); 1470 } 1471 1472 function removeThinkingDots(botId) { 1473 botId = botId || 'default'; 1474 var $chatBox = getElement(botId, 'chat-box'); 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove(); 1476 } 1477 1478 // ==================================== 1479 // TEXT FORMATTING & PROCESSING 1480 // ==================================== 1481 1482 function linkify(inputText) { 1483 if (!inputText) { 1484 return ''; 1485 } 1486 1487 // Helper function to check if URL is already encoded 1488 function isUrlEncoded(url) { 1489 // Check for % followed by exactly 2 hex digits 1490 return /%[0-9a-fA-F]{2}/.test(url); 1491 } 1492 1493 // Helper function to safely encode URLs only if needed 1494 function safeEncodeUrl(url) { 1495 // If URL already contains encoded characters, return as-is 1496 if (isUrlEncoded(url)) { 1497 return url; 1498 } 1499 // Otherwise, encode it 1500 return encodeURI(url); 1501 } 1502 1503 // Process markdown headers FIRST 1504 let processedText = formatMarkdownHeaders(inputText); 1505 1506 // Process text styling (bold, italic, strikethrough) 1507 processedText = formatTextStyling(processedText); 1508 1509 // Process code blocks BEFORE processing links 1510 processedText = formatCodeBlocks(processedText); 1511 1512 // NOW convert to paragraphs 1513 processedText = convertNewlinesToBreaks(processedText); 1514 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL] 1516 // This prevents them from being processed as markdown links 1517 // Match [URL] where URL is a complete URL in square brackets (common in AI citations) 1518 processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => { 1519 // Clean the URL of any trailing punctuation 1520 let cleanUrl = url.replace(/[.,;!?]+$/, ''); 1521 const safeUrl = safeEncodeUrl(cleanUrl); 1522 // Return as a proper link without the brackets 1523 return `<a href="${safeUrl}" target="${linkTarget}">
1523${cleanUrl}</a>`; 1524 }); 1525 1526 // Process proper markdown links with text: [text](url) 1527 // This MUST have non-empty text in the first brackets 1528 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; 1529 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => { 1530 // Make sure we have actual text (not just whitespace) 1531 if (!text || !text.trim()) { 1532 // If no text, treat the URL as the text 1533 let cleanUrl = url.replace(/[.,;!?]+$/, ''); 1534 const safeUrl = safeEncodeUrl(cleanUrl); 1535 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`; 1536 } 1537 1538 // Clean the URL 1539 let cleanUrl = url.replace(/[\].,;!?]+$/, ''); 1540 const safeUrl = safeEncodeUrl(cleanUrl); 1541 const safeText = sanitizeUserInput(text); 1542 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`; 1543 }); 1544 1545 // Handle empty markdown links: [](url) 1546 // This is a specific case where there's no text 1547 const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g; 1548 processedText = processedText.replace(emptyMarkdownPattern, (match, url) => { 1549 let cleanUrl = url.replace(/[.,;!?]+$/, ''); 1550 const safeUrl = safeEncodeUrl(cleanUrl); 1551 // Use the URL itself as the link text 1552 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`; 1553 }); 1554 1555 // Process phone numbers: [text](tel:number) 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g; 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => { 1558 const safePhone = safeEncodeUrl(phone); 1559 const safeText = sanitizeUserInput(text); 1560 return `<a href="${safePhone}">${safeText}</a>`; 1561 }); 1562 1563 // Process mailto links: [text](mailto:email) 1564 const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g; 1565 processedText = processedText.replace(mailtoPattern, (match, text, mailto) => { 1566 const safeMailto = safeEncodeUrl(mailto); 1567 const safeText = sanitizeUserInput(text); 1568 return `<a href="${safeMailto}">${safeText}</a>`; 1569 }); 1570 1571 // Process standalone URLs - but NOT if they're already in <a> tags or brackets 1572 // Updated pattern to be more careful about what it matches 1573 const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim; 1574 processedText = processedText.replace(urlPattern, (match, prefix, url) => { 1575 // Extra check: make sure this isn't already linked 1576 if (match.includes('href=') || match.includes('</a>')) { 1577 return match; 1578 } 1579 1580 // Clean trailing punctuation 1581 let cleanUrl = url.replace(/[.,;!?)]+$/, ''); 1582 const safeUrl = safeEncodeUrl(cleanUrl); 1583 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`; 1584 }); 1585 1586 // Process www. URLs - but NOT if they're already in <a> tags or brackets 1587 const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim; 1588 processedText = processedText.replace(wwwPattern, (match, prefix, url) => { 1589 // Extra check: make sure this isn't already linked 1590 if (match.includes('href=') || match.includes('</a>')) { 1591 return match; 1592 } 1593 1594 // Clean trailing punctuation 1595 let cleanUrl = url.replace(/[.,;!?)]+$/, ''); 1596 const safeUrl = safeEncodeUrl(`http://${cleanUrl}`); 1597 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`; 1598 }); 1599 1600 return processedText; 1601} 1602 1603 function formatMarkdownHeaders(text) { 1604 // Handle h1 to h6 headers 1605 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) { 1606 const level = hashes.length; 1607 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`; 1608 }); 1609 } 1610 1611function formatTextStyling(text) { 1612 // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting 1613 const protectedSegments = []; 1614 let protectedText = text; 1615 1616 // Step 1a: Protect HTML href="..." attributes 1617 protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) { 1618 const placeholder = `__PROTECTED_${protectedSegments.length}__`; 1619 protectedSegments.push(match); 1620 return placeholder; 1621 });
1622 1623 // Step 1b: Protect Markdown links [text](url) 1624 // This is crucial - we need to protect the URLs in markdown format 1625 protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) { 1626 const placeholder = `__PROTECTED_${protectedSegments.length}__`; 1627 protectedSegments.push(match); 1628 return placeholder; 1629 }); 1630 1631 // Step 1c: Also protect bare URLs that might exist 1632 protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) { 1633 const placeholder = `__PROTECTED_${protectedSegments.length}__`; 1634 protectedSegments.push(match); 1635 return placeholder; 1636 }); 1637 1638 // Step 2: Now apply text styling to the protected text 1639 // Handle bold text (**text**) 1640 protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); 1641 1642 // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind) 1643 // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another * 1644 protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>'); 1645 1646 // Handle underscores for italic - Safari-compatible (no lookbehind) 1647 // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED 1648 protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>'); 1649 1650 // Handle strikethrough (~~text~~) 1651 protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>'); 1652 1653 // Step 3: Restore all protected segments 1654 protectedSegments.forEach((original, index) => { 1655 const placeholder = `__PROTECTED_${index}__`; 1656 protectedText = protectedText.replace(placeholder, original); 1657 }); 1658 1659 return protectedText; 1660} 1661 function formatBoldText(text) { 1662 // This function is kept for compatibility but now uses formatTextStyling 1663 return formatTextStyling(text); 1664 } 1665 1666function convertNewlinesToBreaks(text) { 1667 // Split the text into paragraphs (marked by double newlines or multiple <br> tags) 1668 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g); 1669 1670 // Filter out empty paragraphs and wrap each paragraph in <p> tags 1671 return paragraphs 1672 .map(para => para.trim()) 1673 .filter(para => para.length > 0) // Remove empty paragraphs 1674 .map(para => `<p>${para}</p>`) 1675 .join(''); 1676} 1677 function formatCodeBlocks(text) { 1678 // Handle fenced code blocks with language specification (```language) 1679 text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => { 1680 const lang = language || 'text'; 1681 const escapedCode = escapeHtml(code.trim()); 1682 return `<div class="mxchat-code-block-container"> 1683 <div class="mxchat-code-header"> 1684 <span class="mxchat-code-language">${lang}</span> 1685 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button> 1686 </div> 1687 <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre> 1688 </div>`; 1689 }); 1690 1691 // Handle inline code with single backticks 1692 text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>'); 1693 1694 // Handle raw PHP tags (legacy support) 1695 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => { 1696 const escapedCode = escapeHtml(match); 1697 return `<div class="mxchat-code-block-container"> 1698 <div class="mxchat-code-header"> 1699 <span class="mxchat-code-language">php</span> 1700 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button> 1701 </div> 1702 <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre> 1703 </div>`; 1704 }); 1705 1706 return text; 1707 } 1708 1709 function sanitizeUserInput(text) { 1710 const div = document.createElement('div'); 1711 div.textContent = text; 1712 return div.innerHTML; 1713 } 1714 1715 function escapeHtml(unsafe) { 1716 // Skip escaping if it's already escaped or contains HTML code block markup 1717 if (unsafe.includes('<') || unsafe.includes('>') || 1718 unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) { 1719 return unsafe; 1720 } 1721 1722 return unsafe 1723 .replace(/&/g, "&") 1724 .replace(/</g, "<") 1725 .replace(/>/g, ">") 1726 .replace(/"/g, """) 1727 .replace(/'/g, "'"); 1728 } 1729 1730 function decodeHTMLEntities(text) { 1731 var textArea = document.createElement('textarea'); 1732 textArea.innerHTML = text; 1733 return textArea.value; 1734 } 1735 1736 // ==================================== 1737 // UI & SCROLLING CONTROLS 1738 // ==================================== 1739 1740 function scrollToBottom(botIdOrInstant, instant) { 1741 // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false) 1742 var botId = 'default'; 1743 if (typeof botIdOrInstant === 'string') { 1744 botId = botIdOrInstant; 1745 instant = instant || false; 1746 } else if (typeof botIdOrInstant === 'boolean') { 1747 instant = botIdOrInstant; 1748 } else { 1749 instant = false; 1750 } 1751 1752 var chatBox = getElement(botId, 'chat-box'); 1753 if (instant) { 1754 // Instantly set the scroll position to the bottom 1755 chatBox.scrollTop(chatBox.prop("scrollHeight")); 1756 } else { 1757 // Use requestAnimationFrame for smoother scrolling if needed 1758 let start = null; 1759 const scrollHeight = chatBox.prop("scrollHeight"); 1760 const initialScroll = chatBox.scrollTop(); 1761 const distance = scrollHeight - initialScroll; 1762 const duration = 500; // Duration in ms 1763 1764 function smoothScroll(timestamp) { 1765 if (!start) start = timestamp; 1766 const progress = timestamp - start; 1767 const currentScroll = initialScroll + (distance * (progress / duration)); 1768 chatBox.scrollTop(currentScroll); 1769 1770 if (progress < duration) { 1771 requestAnimationFrame(smoothScroll); 1772 } else { 1773 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom 1774 } 1775 } 1776 1777 requestAnimationFrame(smoothScroll); 1778 } 1779 } 1780 1781 function scrollElementToTop(element, botId) { 1782 botId = botId || 'default'; 1783 var chatBox = getElement(botId, 'chat-box'); 1784 var elementTop = element.position().top + chatBox.scrollTop(); 1785 chatBox.animate({ scrollTop: elementTop }, 500); 1786 } 1787 1788 function showChatWidget(botId) { 1789 botId = botId || 'default'; 1790 var $button = getElement(botId, 'floating-chatbot-button'); 1791 // First ensure display is set 1792 $button.css('display', 'flex'); 1793 // Then handle the fade 1794 $button.fadeTo(500, 1); 1795 // Force visibility 1796 $button.removeClass('hidden'); 1797 } 1798 1799 function hideChatWidget(botId) { 1800 botId = botId || 'default'; 1801 var $button = getElement(botId, 'floating-chatbot-button'); 1802 $button.css('display', 'none'); 1803 $button.addClass('hidden'); 1804 } 1805 1806 function disableScroll() { 1807 if (isMobile()) { 1808 $('body').css('overflow', 'hidden'); 1809 } 1810 } 1811 1812 function enableScroll() { 1813 if (isMobile()) { 1814 $('body').css('overflow', ''); 1815 } 1816 } 1817 1818 function isMobile() { 1819 // This can be a simple check, or more sophisticated detection of mobile devices 1820 return window.innerWidth <= 768; // Example threshold for mobile devices 1821 } 1822 1823 function setFullHeight() { 1824 var vh = $(window).innerHeight() * 0.01; 1825 $(':root').css('--vh', vh + 'px'); 1826 } 1827 1828 1829 // ==================================== 1830 // NOTIFICATION SYSTEM 1831 // ==================================== 1832 1833 function createNotificationBadge() { 1834 const chatButton = document.getElementById('floating-chatbot-button'); 1835 1836 if (!chatButton) return; 1837 1838 // Remove any existing badge first 1839 const existingBadge = chatButton.querySelector('.chat-notification-badge'); 1840 if (existingBadge) { 1841 existingBadge.remove(); 1842 } 1843 1844 notificationBadge = document.createElement('div'); 1845 notificationBadge.className = 'chat-notification-badge'; 1846 notificationBadge.style.cssText = ` 1847 display: none; 1848 position: absolute; 1849 top: -5px; 1850 right: -5px; 1851 background-color: red; 1852 color: white; 1853 border-radius: 50%; 1854 padding: 4px 8px; 1855 font-size: 12px; 1856 font-weight: bold; 1857 z-index: 10001; 1858 `; 1859 chatButton.style.position = 'relative'; 1860 chatButton.appendChild(notificationBadge); 1861 1862 } 1863 1864 function showNotification(botId) { 1865 botId = botId || 'default'; 1866 const badge = getElementDOM(botId, 'chat-notification-badge'); 1867 var $floatingChatbot = getElement(botId, 'floating-chatbot'); 1868 if (badge && $floatingChatbot.hasClass('hidden')) { 1869 badge.style.display = 'block'; 1870 badge.textContent = '1'; 1871 } 1872 } 1873 1874 function hideNotification(botId) { 1875 botId = botId || 'default'; 1876 const badge = getElementDOM(botId, 'chat-notification-badge'); 1877 if (badge) { 1878 badge.style.display = 'none'; 1879 } 1880 } 1881 1882 function startNotificationChecking(botId) { 1883 botId = botId || 'default'; 1884 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on'; 1885 if (!chatPersistenceEnabled) return; 1886 1887 createNotificationBadge(botId);
1888 var instance = MxChatInstances.get(botId); 1889 instance.notificationCheckInterval = setInterval(function() { 1890 checkForNewMessages(botId); 1891 }, 30000); // Check every 30 seconds 1892 } 1893 1894 function stopNotificationChecking(botId) { 1895 botId = botId || 'default'; 1896 var instance = MxChatInstances.get(botId); 1897 if (instance.notificationCheckInterval) { 1898 clearInterval(instance.notificationCheckInterval); 1899 } 1900 } 1901 1902 function checkForNewMessages() { 1903 const sessionId = getChatSession(); 1904 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on'; 1905 1906 if (!chatPersistenceEnabled) return; 1907 1908 $.ajax({ 1909 url: mxchatChat.ajax_url, 1910 type: 'POST', 1911 data: { 1912 action: 'mxchat_check_new_messages', 1913 session_id: sessionId, 1914 last_seen_id: lastSeenMessageId, 1915 nonce: mxchatChat.nonce 1916 }, 1917 success: function(response) { 1918 if (response.success && response.data.hasNewMessages) { 1919 showNotification(); 1920 } 1921 } 1922 }); 1923 } 1924 1925 1926// ==================================== 1927// LIVE AGENT FUNCTIONALITY 1928// ==================================== 1929 1930function startPolling(botId) { 1931 console.log('[MxChat] startPolling called for botId:', botId); 1932 botId = botId || 'default'; 1933 var instance = MxChatInstances.get(botId); 1934 // Clear any existing interval first 1935 stopPolling(botId); 1936 // Start new polling interval 1937 console.log('[MxChat] Starting polling interval (5s) for botId:', botId); 1938 instance.pollingInterval = setInterval(function() { 1939 checkForAgentMessages(botId); 1940 }, 5000); 1941} 1942 1943function stopPolling(botId) { 1944 console.log('[MxChat] stopPolling called for botId:', botId); 1945 botId = botId || 'default'; 1946 var instance = MxChatInstances.get(botId); 1947 if (instance.pollingInterval) { 1948 clearInterval(instance.pollingInterval); 1949 instance.pollingInterval = null; 1950 console.log('[MxChat] Polling stopped for botId:', botId); 1951 } 1952} 1953 1954function checkForAgentMessages(botId) { 1955 console.log('[MxChat] checkForAgentMessages called for botId:', botId); 1956 botId = botId || 'default'; 1957 var instance = MxChatInstances.get(botId); 1958 const sessionId = getChatSession(botId); 1959 $.ajax({
1960 url: mxchatChat.ajax_url, 1961 type: 'POST', 1962 dataType: 'json', 1963 data: { 1964 action: 'mxchat_fetch_new_messages', 1965 session_id: sessionId, 1966 last_seen_id: instance.lastSeenMessageId, 1967 persistence_enabled: 'true', 1968 nonce: mxchatChat.nonce 1969 }, 1970 success: function (response) { 1971 if (response.success && response.data?.new_messages) { 1972 let hasNewMessage = false; 1973 1974 response.data.new_messages.forEach(function (message) { 1975 if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) { 1976 hasNewMessage = true; 1977 appendMessage("agent", message.content, '', [], false, botId); 1978 instance.lastSeenMessageId = message.id; 1979 instance.processedMessageIds.add(message.id); 1980 } 1981 }); 1982 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot'); 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) { 1985 showNotification(botId); 1986 } 1987 1988 scrollToBottom(botId, true); 1989 } 1990 }, 1991 error: function (xhr, status, error) { 1992 // Polling error - silently continue 1993 } 1994 }); 1995} 1996 1997 // ==================================== 1998 // CHAT HISTORY & PERSISTENCE 1999 // ==================================== 2000 2001function loadChatHistory(botId) { 2002 botId = botId || 'default'; 2003 var instance = MxChatInstances.get(botId); 2004 2005 // Prevent duplicate loading 2006 if (instance.chatHistoryLoaded) { 2007 return; 2008 } 2009 2010 var sessionId = getChatSession(botId); 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on'; 2012 2013 if (chatPersistenceEnabled && sessionId) { 2014 $.ajax({
2015 url: mxchatChat.ajax_url, 2016 type: 'POST', 2017 dataType: 'json', 2018 data: { 2019 action: 'mxchat_fetch_conversation_history', 2020 session_id: sessionId 2021 }, 2022 success: function(response) { 2023 // Handle session reset (IP changed while user was away) 2024 if (response.success === false && response.data && response.data.action === 'reset_session') { 2025 // Silently reset session - user will start fresh 2026 resetChatSession(botId); 2027 instance.chatHistoryLoaded = true; // Prevent retry loop 2028 return; 2029 } 2030 2031 // Check if the response indicates success 2032 if (response.success) { 2033 // Handle case where conversation data exists and is an array 2034 if (response.data && Array.isArray(response.data.conversation)) { 2035 var $chatBox = getElement(botId, 'chat-box'); 2036 var $fragment = $(document.createDocumentFragment()); 2037 let highestMessageId = instance.lastSeenMessageId; 2038 2039 // Update chat mode if provided 2040 if (response.data.chat_mode) { 2041 updateChatModeIndicator(response.data.chat_mode, botId); 2042 } 2043 2044 // Only process if there are actual messages 2045 if (response.data.conversation.length > 0) { 2046 // IMPORTANT: Clear existing messages before loading history 2047 $chatBox.empty(); 2048 2049 $.each(response.data.conversation, function(index, message) { 2050 // Skip agent messages if persistence is off 2051 if (!chatPersistenceEnabled && message.role === 'agent') { 2052 return; 2053 } 2054 2055 var messageClass, messageBgColor, messageFontColor; 2056 2057 switch (message.role) { 2058 case 'user': 2059 messageClass = 'user-message'; 2060 messageBgColor = userMessageBgColor; 2061 messageFontColor = userMessageFontColor; 2062 break; 2063 case 'agent': 2064 messageClass = 'agent-message'; 2065 messageBgColor = liveAgentMessageBgColor; 2066 messageFontColor = liveAgentMessageFontColor; 2067 break; 2068 default: 2069 messageClass = 'bot-message'; 2070 messageBgColor = botMessageBgColor; 2071 messageFontColor = botMessageFontColor; 2072 break; 2073 } 2074 2075 var messageElement = $('<div>').addClass(messageClass) 2076 .css({ 2077 'background': messageBgColor, 2078 'color': messageFontColor 2079 }); 2080 2081 var content = message.content; 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"'); 2083 content = decodeHTMLEntities(content); 2084 2085 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) { 2086 messageElement.html(content); 2087 } else { 2088 var formattedContent = linkify(content); 2089 messageElement.html(formattedContent); 2090 } 2091 2092 $fragment.append(messageElement); 2093 2094 // Track message IDs 2095 if (message.id) {
2096 highestMessageId = Math.max(highestMessageId, message.id); 2097 instance.processedMessageIds.add(message.id); 2098 } 2099 }); 2100 2101 // Only append messages and scroll if we have content 2102 $chatBox.append($fragment); 2103 scrollToBottom(botId, true); 2104 2105 // Collapse quick questions if we have conversation history 2106 // BUT skip auto-collapse for embedded bots (they should stay expanded) 2107 if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) { 2108 collapseQuickQuestions(botId); 2109 } 2110 2111 // Update lastSeenMessageId after history loads 2112 instance.lastSeenMessageId = highestMessageId; 2113 2114 // Only update chat mode if persistence is enabled and we have messages 2115 if (chatPersistenceEnabled) { 2116 var lastMessage = response.data.conversation[response.data.conversation.length - 1]; 2117 if (lastMessage.role === 'agent') { 2118 updateChatModeIndicator('agent', botId); 2119 } 2120 } 2121 2122 // Mark as loaded ONLY after successful load 2123 instance.chatHistoryLoaded = true; 2124 } 2125 } 2126 } 2127 }, 2128 error: function(xhr, status, error) { 2129 // Error loading chat history - silently continue 2130 } 2131 }); 2132 } 2133} 2134 2135 2136 // ==================================== 2137 // FILE UPLOAD FUNCTIONALITY 2138 // ==================================== 2139 2140 function addSafeEventListener(elementId, eventType, handler) { 2141 const element = document.getElementById(elementId); 2142 if (element) { 2143 element.addEventListener(eventType, handler); 2144 } 2145 } 2146 2147 function showActivePdf(filename, botId) { 2148 botId = botId || 'default'; 2149 const container = getElementDOM(botId, 'active-pdf-container'); 2150 const nameElement = getElementDOM(botId, 'active-pdf-name'); 2151 2152 if (!container || !nameElement) { 2153 return; 2154 } 2155 2156 nameElement.textContent = filename; 2157 container.style.display = 'flex'; 2158 } 2159 2160 function showActiveWord(filename, botId) { 2161 botId = botId || 'default'; 2162 const container = getElementDOM(botId, 'active-word-container'); 2163 const nameElement = getElementDOM(botId, 'active-word-name'); 2164 2165 if (!container || !nameElement) { 2166 return; 2167 } 2168 2169 nameElement.textContent = filename; 2170 container.style.display = 'flex'; 2171 } 2172 2173 function removeActivePdf(botId) { 2174 botId = botId || 'default'; 2175 var instance = MxChatInstances.get(botId); 2176 const container = getElementDOM(botId, 'active-pdf-container'); 2177 const nameElement = getElementDOM(botId, 'active-pdf-name'); 2178 2179 if (!container || !nameElement || !instance.activePdfFile) return; 2180 2181 fetch(mxchatChat.ajax_url, { 2182 method: 'POST', 2183 headers: { 2184 'Content-Type': 'application/x-www-form-urlencoded', 2185 }, 2186 body: new URLSearchParams({ 2187 'action': 'mxchat_remove_pdf', 2188 'session_id': getChatSession(botId), 2189 'nonce': mxchatChat.nonce 2190 }) 2191 }) 2192 .then(response => response.json()) 2193 .then(data => { 2194 if (data.success) { 2195 container.style.display = 'none'; 2196 nameElement.textContent = ''; 2197 activePdfFile = null; 2198 appendMessage('bot', 'PDF removed.'); 2199 } 2200 }) 2201 .catch(error => { 2202 // Error removing PDF - silently continue 2203 }); 2204 } 2205 2206 function removeActiveWord() { 2207 const container = document.getElementById('active-word-container'); 2208 const nameElement = document.getElementById('active-word-name'); 2209 2210 if (!container || !nameElement || !activeWordFile) return; 2211 2212 fetch(mxchatChat.ajax_url, { 2213 method: 'POST', 2214 headers: { 2215 'Content-Type': 'application/x-www-form-urlencoded', 2216 }, 2217 body: new URLSearchParams({ 2218 'action': 'mxchat_remove_word', 2219 'session_id': sessionId, 2220 'nonce': mxchatChat.nonce 2221 }) 2222 }) 2223 .then(response => response.json()) 2224 .then(data => { 2225 if (data.success) { 2226 container.style.display = 'none'; 2227 nameElement.textContent = ''; 2228 activeWordFile = null; 2229 appendMessage('bot', 'Word document removed.'); 2230 } 2231 }) 2232 .catch(error => { 2233 // Error removing Word document - silently continue 2234 }); 2235 } 2236 2237 // ==================================== 2238 // CONSENT & COMPLIANCE (GDPR) 2239 // ==================================== 2240 2241 function initializeChatVisibility(botId) { 2242 botId = botId || 'default'; 2243 const complianzEnabled = mxchatChat.complianz_toggle === 'on' || 2244 mxchatChat.complianz_toggle === '1' || 2245 mxchatChat.complianz_toggle === 1; 2246 2247 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') { 2248 // Initial check 2249 checkConsentAndShowChat(botId); 2250 2251 // Listen for consent changes 2252 $(document).on('cmplz_status_change', function(event) { 2253 checkConsentAndShowChat(botId); 2254 }); 2255 } else { 2256 // If Complianz is not enabled, always show 2257 getElement(botId, 'floating-chatbot-button') 2258 .css('display', 'flex') 2259 .removeClass('hidden no-consent') 2260 .fadeTo(500, 1); 2261 2262 // Also check pre-chat message when Complianz is not enabled 2263 checkPreChatDismissal(botId); 2264 } 2265 } 2266 2267 2268 function checkConsentAndShowChat(botId) { 2269 botId = botId || 'default'; 2270 var consentStatus = cmplz_has_consent('marketing'); 2271 var consentType = complianz.consenttype; 2272 2273 let $widget = getElement(botId, 'floating-chatbot-button'); 2274 let $chatbot = getElement(botId, 'floating-chatbot'); 2275 let $preChat = getElement(botId, 'pre-chat-message'); 2276 2277 if (consentStatus === true) { 2278 $widget 2279 .removeClass('no-consent') 2280 .css('display', 'flex') 2281 .removeClass('hidden') 2282 .fadeTo(500, 1); 2283 $chatbot.removeClass('no-consent'); 2284 2285 // Show pre-chat message if not dismissed 2286 checkPreChatDismissal(botId); 2287 } else { 2288 $widget 2289 .addClass('no-consent') 2290 .fadeTo(500, 0, function() { 2291 $(this) 2292 .css('display', 'none') 2293 .addClass('hidden'); 2294 }); 2295 $chatbot.addClass('no-consent'); 2296 2297 // Hide pre-chat message when no consent
2298 $preChat.hide(); 2299 } 2300 } 2301 2302 2303 // ==================================== 2304 // PRE-CHAT MESSAGE HANDLING 2305 // ==================================== 2306 2307 function checkPreChatDismissal(botId) { 2308 botId = botId || 'default'; 2309 $.ajax({ 2310 url: mxchatChat.ajax_url, 2311 type: 'POST', 2312 data: { 2313 action: 'mxchat_check_pre_chat_message_status', 2314 _ajax_nonce: mxchatChat.nonce 2315 }, 2316 success: function(response) { 2317 if (response.success && !response.data.dismissed) { 2318 getElement(botId, 'pre-chat-message').fadeIn(250); 2319 } else { 2320 getElement(botId, 'pre-chat-message').hide(); 2321 } 2322 }, 2323 error: function() { 2324 // Error checking pre-chat dismissal - silently continue 2325 } 2326 }); 2327 } 2328 2329 function handlePreChatDismissal(botId) { 2330 botId = botId || 'default'; 2331 getElement(botId, 'pre-chat-message').fadeOut(200); 2332 $.ajax({
2333 url: mxchatChat.ajax_url, 2334 type: 'POST', 2335 data: { 2336 action: 'mxchat_dismiss_pre_chat_message', 2337 _ajax_nonce: mxchatChat.nonce 2338 }, 2339 success: function() { 2340 $('#pre-chat-message').hide(); 2341 }, 2342 error: function() { 2343 // Error dismissing pre-chat message - silently continue 2344 } 2345 }); 2346 } 2347 2348 2349 // ==================================== 2350 // UTILITY FUNCTIONS 2351 // ==================================== 2352 2353 function copyToClipboard(text) { 2354 var tempInput = $('<input>'); 2355 $('body').append(tempInput); 2356 tempInput.val(text).select(); 2357 document.execCommand('copy'); 2358 tempInput.remove(); 2359 } 2360 2361 2362 function isImageHtml(str) { 2363 return str.startsWith('<img') && str.endsWith('>'); 2364 } 2365 2366 2367 // ==================================== 2368 // EVENT HANDLERS & INITIALIZATION 2369 // ==================================== 2370 2371$(document).on('click', '.mxchat-popular-question', function () { 2372 var question = $(this).text(); 2373 var botId = getBotIdFromElement(this); 2374 2375 // Append the question as if the user typed it 2376 appendMessage("user", question, '', [], false, botId); 2377 2378 // Only collapse if there are questions 2379 if (hasQuickQuestions(botId)) { 2380 collapseQuickQuestions(botId); 2381 } 2382 2383 // Send the question to the server 2384 sendMessageToChatbot(question, botId); 2385}); 2386 2387$(document).on('click', '.questions-toggle-btn', function(e) { 2388 e.preventDefault(); 2389 e.stopPropagation(); 2390 var botId = getBotIdFromElement(this); 2391 expandQuickQuestions(botId); 2392}); 2393 2394$(document).on('click', '.questions-collapse-btn', function(e) { 2395 e.preventDefault(); 2396 e.stopPropagation(); 2397 var botId = getBotIdFromElement(this); 2398 collapseQuickQuestions(botId); 2399}); 2400 2401 // Chatbot visibility toggle handlers - use class selector for multi-instance support 2402 $(document).on('click', '.floating-chatbot-button', function() { 2403 var botId = getBotIdFromElement(this); 2404 var $chatbot = getElement(botId, 'floating-chatbot'); 2405 var $badge = getElement(botId, 'chat-notification-badge'); 2406 var $preChat = getElement(botId, 'pre-chat-message'); 2407 2408 if ($chatbot.hasClass('hidden')) { 2409 $chatbot.removeClass('hidden').addClass('visible'); 2410 $(this).addClass('hidden'); 2411 $badge.hide(); // Hide notification when opening chat 2412 disableScroll(); 2413 $preChat.fadeOut(250); 2414 } else { 2415 $chatbot.removeClass('visible').addClass('hidden'); 2416 $(this).removeClass('hidden'); 2417 enableScroll(); 2418 checkPreChatDismissal(botId); 2419 } 2420 }); 2421 2422 // Allow clicking anywhere on the title bar to close the chatbot 2423 $(document).on('click', '.chatbot-top-bar', function() { 2424 var botId = getBotIdFromElement(this); 2425 getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible'); 2426 getElement(botId, 'floating-chatbot-button').removeClass('hidden'); 2427 enableScroll(); 2428 }); 2429 2430 $(document).on('click', '.close-pre-chat-message', function(e) { 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click 2432 var botId = getBotIdFromElement(this); 2433 getElement(botId, 'pre-chat-message').fadeOut(200, function() { 2434 $(this).remove(); 2435 }); 2436 }); 2437 2438 2439 // PDF upload button handlers - use class selector 2440 $(document).on('click', '.pdf-upload-btn', function() { 2441 var botId = getBotIdFromElement(this); 2442 var pdfInput = getElementDOM(botId, 'pdf-upload'); 2443 if (pdfInput) pdfInput.click(); 2444 }); 2445 2446 // Word upload button handlers - use class selector 2447 $(document).on('click', '.word-upload-btn', function() { 2448 var botId = getBotIdFromElement(this); 2449 var wordInput = getElementDOM(botId, 'word-upload'); 2450 if (wordInput) wordInput.click(); 2451 });
2452 2453 // PDF file input change handler 2454 addSafeEventListener('pdf-upload', 'change', async function(e) { 2455 const file = e.target.files[0]; 2456 2457 if (!file || file.type !== 'application/pdf') { 2458 alert('Please select a valid PDF file.'); 2459 return; 2460 } 2461 2462 if (!sessionId) { 2463 alert('Error: No session ID found'); 2464 return; 2465 } 2466 2467 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) { 2468 alert('Error: Ajax configuration missing'); 2469 return; 2470 } 2471 2472 // Disable buttons and show loading state 2473 const uploadBtn = document.getElementById('pdf-upload-btn'); 2474 const sendBtn = document.getElementById('send-button'); 2475 const originalBtnContent = uploadBtn.innerHTML; 2476 2477 try { 2478 const formData = new FormData(); 2479 formData.append('action', 'mxchat_upload_pdf'); 2480 formData.append('pdf_file', file); 2481 formData.append('session_id', sessionId); 2482 formData.append('nonce', mxchatChat.nonce); 2483 2484 uploadBtn.disabled = true; 2485 sendBtn.disabled = true; 2486 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50"> 2487 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle> 2488 </svg>`; 2489 2490 const response = await fetch(mxchatChat.ajax_url, { 2491 method: 'POST', 2492 body: formData 2493 }); 2494 2495 const data = await response.json(); 2496 2497 if (data.success) { 2498 // Hide popular questions if they exist 2499 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions'); 2500 if (hasQuickQuestions()) { 2501 collapseQuickQuestions(); 2502 } 2503 2504 // Show the active PDF name 2505 showActivePdf(data.data.filename); 2506 2507 appendMessage('bot', data.data.message); 2508 scrollToBottom(); 2509 activePdfFile = data.data.filename; 2510 } else { 2511 alert('Failed to upload PDF. Please try again.'); 2512 } 2513 } catch (error) { 2514 alert('Error uploading file. Please try again.'); 2515 } finally { 2516 uploadBtn.disabled = false; 2517 sendBtn.disabled = false; 2518 uploadBtn.innerHTML = originalBtnContent; 2519 this.value = ''; // Reset file input 2520 } 2521 }); 2522 2523 // Word file input change handler 2524 addSafeEventListener('word-upload', 'change', async function(e) { 2525 const file = e.target.files[0]; 2526 2527 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { 2528 alert('Please select a valid Word document (.docx).'); 2529 return; 2530 } 2531 2532 if (!sessionId) { 2533 alert('Error: No session ID found'); 2534 return; 2535 } 2536 2537 // Disable buttons and show loading state 2538 const uploadBtn = document.getElementById('word-upload-btn'); 2539 const sendBtn = document.getElementById('send-button'); 2540 const originalBtnContent = uploadBtn.innerHTML; 2541 2542 try { 2543 const formData = new FormData(); 2544 formData.append('action', 'mxchat_upload_word'); 2545 formData.append('word_file', file); 2546 formData.append('session_id', sessionId); 2547 formData.append('nonce', mxchatChat.nonce); 2548 2549 uploadBtn.disabled = true; 2550 sendBtn.disabled = true; 2551 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50"> 2552 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle> 2553 </svg>`; 2554 2555 const response = await fetch(mxchatChat.ajax_url, { 2556 method: 'POST', 2557 body: formData 2558 }); 2559 2560 const data = await response.json(); 2561 2562 if (data.success) { 2563 // Hide popular questions if they exist 2564 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions'); 2565 if (hasQuickQuestions()) { 2566 collapseQuickQuestions(); 2567 } 2568 2569 // Show the active Word document name 2570 showActiveWord(data.data.filename); 2571 2572 appendMessage('bot', data.data.message); 2573 scrollToBottom(); 2574 activeWordFile = data.data.filename; 2575 } else { 2576 alert('Failed to upload Word document. Please try again.'); 2577 } 2578 } catch (error) { 2579 alert('Error uploading file. Please try again.'); 2580 } finally { 2581 uploadBtn.disabled = false; 2582 sendBtn.disabled = false; 2583 uploadBtn.innerHTML = originalBtnContent; 2584 this.value = ''; // Reset file input 2585 } 2586 });
2587 2588 // Remove button click handlers 2589 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) { 2590 e.preventDefault(); 2591 e.stopPropagation(); 2592 removeActivePdf(); 2593 }); 2594 2595 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) { 2596 e.preventDefault(); 2597 e.stopPropagation(); 2598 removeActiveWord(); 2599 }); 2600 2601 // Window resize handlers 2602 $(window).on('resize orientationchange', function() { 2603 setFullHeight(); 2604 }); 2605 2606 2607 // ==================================== 2608 // TOOLBAR & STYLING SETUP 2609 // ==================================== 2610 2611 // Apply toolbar settings 2612 if (mxchatChat.chat_toolbar_toggle === 'on') { 2613 $('.chat-toolbar').show(); 2614 } else { 2615 $('.chat-toolbar').hide(); 2616 } 2617 2618 // Apply toolbar icon colors 2619 const toolbarElements = [ 2620 '#mxchat-chatbot .toolbar-btn svg', 2621 '#mxchat-chatbot .active-pdf-name', 2622 '#mxchat-chatbot .active-word-name', 2623 '#mxchat-chatbot .remove-pdf-btn svg', 2624 '#mxchat-chatbot .remove-word-btn svg', 2625 '#mxchat-chatbot .toolbar-perplexity svg' 2626 ]; 2627 2628 toolbarElements.forEach(selector => { 2629 $(selector).css({ 2630 'fill': toolbarIconColor, 2631 'stroke': toolbarIconColor, 2632 'color': toolbarIconColor 2633 }); 2634 }); 2635 2636 2637// ==================================== 2638// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION 2639// ==================================== 2640// Only run email collection setup if it's enabled 2641if (mxchatChat && mxchatChat.email_collection_enabled === 'on') { 2642 2643 // Track submitting state per bot 2644 const emailSubmittingState = {}; 2645 2646 // Add CSS animations for email form (once globally) 2647 if (!document.getElementById('email-error-styles')) { 2648 const style = document.createElement('style'); 2649 style.id = 'email-error-styles'; 2650 style.textContent = ` 2651 @keyframes fadeInError { 2652 from { opacity: 0; transform: translateY(-5px); } 2653 to { opacity: 1; transform: translateY(0); } 2654 } 2655 .email-input-shake { 2656 animation: shake 0.5s ease-in-out; 2657 } 2658 @keyframes shake { 2659 0%, 100% { transform: translateX(0); } 2660 25% { transform: translateX(-5px); } 2661 75% { transform: translateX(5px); } 2662 } 2663 @keyframes spin { 2664 from { transform: rotate(0deg); } 2665 to { transform: rotate(360deg); } 2666 } 2667 .email-spinner { 2668 display: inline-block; 2669 vertical-align: middle; 2670 } 2671 `; 2672 document.head.appendChild(style); 2673 } 2674 2675 // Helper functions for email collection (multi-instance aware) 2676 function showEmailFormForBot(botId) { 2677 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2678 var chatContainer = getElementDOM(botId, 'chat-container'); 2679 if (emailBlocker) emailBlocker.style.display = 'flex'; 2680 if (chatContainer) chatContainer.style.display = 'none'; 2681 } 2682 2683 function showChatContainerForBot(botId) { 2684 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2685 var chatContainer = getElementDOM(botId, 'chat-container'); 2686 if (emailBlocker) emailBlocker.style.display = 'none'; 2687 if (chatContainer) chatContainer.style.display = 'flex'; 2688 2689 // Load chat history for this bot 2690 if (typeof loadChatHistory === 'function') { 2691 loadChatHistory(botId); 2692 } 2693 } 2694 2695 function isValidEmailAddress(email) { 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 2697 return emailRegex.test(email.trim()) && email.length <= 254; 2698 } 2699 2700 function isValidNameInput(name) { 2701 return name && name.trim().length >= 2 && name.trim().length <= 100; 2702 } 2703 2704 function setEmailSubmissionState(botId, loading) { 2705 var submitButton = getElementDOM(botId, 'email-submit-button'); 2706 var emailInput = getElementDOM(botId, 'user-email'); 2707 var nameInput = getElementDOM(botId, 'user-name'); 2708 2709 if (loading) { 2710 emailSubmittingState[botId] = true; 2711 if (submitButton) submitButton.disabled = true; 2712 if (emailInput) emailInput.disabled = true; 2713 if (nameInput) nameInput.disabled = true; 2714 2715 if (submitButton && !submitButton.getAttribute('data-original-html')) { 2716 submitButton.setAttribute('data-original-html', submitButton.innerHTML); 2717 const originalText = submitButton.textContent; 2718 submitButton.innerHTML = ` 2719 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24"> 2720 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416"> 2721 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;
27210 31.416" repeatCount="indefinite"/> 2722 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/> 2723 </circle> 2724 </svg> 2725 ${originalText} 2726 `; 2727 submitButton.style.opacity = '0.8'; 2728 } 2729 } else { 2730 emailSubmittingState[botId] = false; 2731 if (submitButton) submitButton.disabled = false; 2732 if (emailInput) emailInput.disabled = false; 2733 if (nameInput) nameInput.disabled = false; 2734 2735 if (submitButton) { 2736 const originalHtml = submitButton.getAttribute('data-original-html'); 2737 if (originalHtml) { 2738 submitButton.innerHTML = originalHtml; 2739 } 2740 submitButton.style.opacity = '1'; 2741 } 2742 } 2743 } 2744 2745 function showEmailError(botId, message) { 2746 clearEmailError(botId); 2747 2748 var emailForm = getElementDOM(botId, 'email-collection-form'); 2749 if (!emailForm) return; 2750 2751 const errorDiv = document.createElement('div'); 2752 errorDiv.className = 'email-error'; 2753 errorDiv.style.cssText = ` 2754 color: #e74c3c; 2755 font-size: 12px; 2756 margin-top: 8px; 2757 padding: 4px 0; 2758 animation: fadeInError 0.3s ease; 2759 `; 2760 errorDiv.textContent = message; 2761 emailForm.appendChild(errorDiv); 2762 2763 // Add shake animation to inputs 2764 var emailInput = getElementDOM(botId, 'user-email'); 2765 var nameInput = getElementDOM(botId, 'user-name'); 2766 2767 if (emailInput) { 2768 emailInput.classList.add('email-input-shake'); 2769 setTimeout(() => emailInput.classList.remove('email-input-shake'), 500); 2770 } 2771 if (nameInput) { 2772 nameInput.classList.add('email-input-shake'); 2773 setTimeout(() => nameInput.classList.remove('email-input-shake'), 500); 2774 } 2775 } 2776 2777 function clearEmailError(botId) { 2778 var emailForm = getElementDOM(botId, 'email-collection-form'); 2779 if (emailForm) { 2780 const existingErrors = emailForm.querySelectorAll('.email-error'); 2781 existingErrors.forEach(error => error.remove()); 2782 } 2783 } 2784 2785 function checkSessionAndEmailForBot(botId) { 2786 const sessionId = getChatSession(botId); 2787 2788 fetch(mxchatChat.ajax_url, { 2789 method: 'POST', 2790 headers: { 2791 'Content-Type': 'application/x-www-form-urlencoded', 2792 }, 2793 body: new URLSearchParams({ 2794 action: 'mxchat_check_email_provided', 2795 session_id: sessionId, 2796 nonce: mxchatChat.nonce, 2797 }) 2798 }) 2799 .then((response) => { 2800 if (!response.ok) { 2801 throw new Error(`HTTP error! status: ${response.status}`); 2802 } 2803 return response.json(); 2804 }) 2805 .then((data) => { 2806 if (data.success) { 2807 if (data.data.logged_in || data.data.email) { 2808 showChatContainerForBot(botId); 2809 } else { 2810 showEmailFormForBot(botId); 2811 } 2812 } else { 2813 showEmailFormForBot(botId); 2814 } 2815 }) 2816 .catch((error) => { 2817 showEmailFormForBot(botId); 2818 }); 2819 } 2820 2821 // Event delegation for email form submission 2822 $(document).on('submit', '.email-collection-form', function(e) { 2823 e.preventDefault(); 2824 e.stopPropagation(); 2825 2826 var botId = getBotIdFromElement(this); 2827 2828 // Prevent double submission 2829 if (emailSubmittingState[botId]) { 2830 return false; 2831 } 2832 2833 var emailInput = getElementDOM(botId, 'user-email'); 2834 var nameInput = getElementDOM(botId, 'user-name'); 2835 var userEmail = emailInput ? emailInput.value.trim() : ''; 2836 var userName = nameInput ? nameInput.value.trim() : ''; 2837 var sessionId = getChatSession(botId); 2838 2839 // Validate email 2840 if (!userEmail) { 2841 showEmailError(botId, 'Please enter your email address.'); 2842 return false; 2843 } 2844 2845 if (!isValidEmailAddress(userEmail)) { 2846 showEmailError(botId, 'Please enter a valid email address.'); 2847 return false; 2848 } 2849 2850 // Validate name if field exists and has content 2851 if (nameInput && userName && !isValidNameInput(userName)) { 2852 showEmailError(botId, 'Please enter a valid name (2-100 characters).'); 2853 return false; 2854 } 2855 2856 clearEmailError(botId); 2857 setEmailSubmissionState(botId, true); 2858 2859 // Prepare form data 2860 const formData = new URLSearchParams({ 2861 action: 'mxchat_handle_save_email_and_response', 2862 email: userEmail, 2863 session_id: sessionId, 2864 nonce: mxchatChat.nonce, 2865 }); 2866 2867 if (userName) { 2868 formData.append('name', userName); 2869 } 2870 2871 fetch(mxchatChat.ajax_url, { 2872 method: 'POST', 2873 headers: { 2874 'Content-Type': 'application/x-www-form-urlencoded', 2875 }, 2876 body: formData 2877 }) 2878 .then((response) => { 2879 if (!response.ok) { 2880 throw new Error(`HTTP error! status: ${response.status}`); 2881 } 2882 return response.json(); 2883 }) 2884 .then((data) => { 2885 setEmailSubmissionState(botId, false); 2886 2887 if (data.success) { 2888 showChatContainerForBot(botId); 2889 2890 if (data.message && typeof appendMessage === 'function') { 2891 setTimeout(() => { 2892 appendMessage('bot', data.message, '', [], false, botId); 2893 if (typeof scrollToBottom === 'function') { 2894 scrollToBottom(botId); 2895 } 2896 }, 100); 2897 } 2898 } else { 2899 showEmailError(botId, data.message || 'Failed to save email. Please try again.'); 2900 } 2901 }) 2902 .catch((error) => { 2903 setEmailSubmissionState(botId, false); 2904 showEmailError(botId, 'An error occurred. Please try again.'); 2905 }); 2906 2907 return false; 2908 });
2909 2910 // Real-time email validation using event delegation 2911 $(document).on('input', '.mxchat-email-input', function() { 2912 var botId = getBotIdFromElement(this); 2913 var $input = $(this); 2914 2915 // Clear previous timeout 2916 clearTimeout($input.data('validationTimeout')); 2917 2918 // Debounce validation 2919 var timeout = setTimeout(() => { 2920 var email = this.value.trim(); 2921 clearEmailError(botId); 2922 2923 if (email && !isValidEmailAddress(email)) { 2924 showEmailError(botId, 'Please enter a valid email address.'); 2925 } 2926 }, 500); 2927 2928 $input.data('validationTimeout', timeout); 2929 }); 2930 2931 // Handle Enter key in email input 2932 $(document).on('keypress', '.mxchat-email-input', function(e) { 2933 if (e.key === 'Enter') { 2934 e.preventDefault(); 2935 var botId = getBotIdFromElement(this); 2936 if (!emailSubmittingState[botId]) { 2937 $(this).closest('.email-collection-form').submit(); 2938 } 2939 } 2940 }); 2941 2942 // Handle Enter key in name input 2943 $(document).on('keypress', '.mxchat-name-input', function(e) { 2944 if (e.key === 'Enter') { 2945 e.preventDefault(); 2946 var botId = getBotIdFromElement(this); 2947 if (!emailSubmittingState[botId]) { 2948 $(this).closest('.email-collection-form').submit(); 2949 } 2950 } 2951 }); 2952 2953 // Initialize email check for all bot instances 2954 $('.mxchat-chatbot-wrapper').each(function() { 2955 var botId = $(this).data('bot-id') || 'default'; 2956 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2957 2958 // Only check if email blocker exists for this bot 2959 if (emailBlocker) { 2960 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) { 2961 if (mxchatChat.initial_email_state.show_email_form) { 2962 showEmailFormForBot(botId); 2963 } else { 2964 showChatContainerForBot(botId); 2965 } 2966 } else { 2967 setTimeout(function() { 2968 checkSessionAndEmailForBot(botId); 2969 }, 100); 2970 } 2971 } 2972 }); 2973} 2974 2975 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance 2976 $(document).on('click', '.pre-chat-message', function() { 2977 var botId = getBotIdFromElement(this); 2978 var $chatbot = getElement(botId, 'floating-chatbot'); 2979 if ($chatbot.hasClass('hidden')) { 2980 $chatbot.removeClass('hidden').addClass('visible'); 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden'); 2982 $(this).fadeOut(250); // Hide pre-chat message 2983 disableScroll(); // Disable scroll when chatbot opens 2984 } 2985 }); 2986 2987 // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376 2988 // This is a fallback for legacy support 2989 $(document).on('click', '.close-pre-chat-message', function() { 2990 var botId = getBotIdFromElement(this); 2991 var $preChat = getElement(botId, 'pre-chat-message'); 2992 $preChat.fadeOut(200); // Hide the message 2993 2994 // Send an AJAX request to set the transient flag for 24 hours 2995 $.ajax({
2996 url: mxchatChat.ajax_url, 2997 type: 'POST', 2998 data: { 2999 action: 'mxchat_dismiss_pre_chat_message', 3000 _ajax_nonce: mxchatChat.nonce 3001 }, 3002 success: function() { 3003 // Ensure the message is hidden after dismissal 3004 $preChat.hide(); 3005 }, 3006 error: function() { 3007 // Error dismissing pre-chat message - silently continue 3008 } 3009 }); 3010 }); 3011 3012 3013function hasQuickQuestions(botId) { 3014 botId = botId || 'default'; 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions'); 3016 if (!questionsContainer) return false; 3017 const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question'); 3018 return questionButtons.length > 0; 3019} 3020 3021/** 3022 * Check if a bot is embedded (not floating) 3023 * Embedded bots don't have a .floating-chatbot wrapper 3024 */ 3025function isEmbeddedBot(botId) { 3026 botId = botId || 'default'; 3027 var floatingWrapper = document.getElementById('floating-chatbot-' + botId); 3028 return !floatingWrapper; 3029} 3030 3031function collapseQuickQuestions(botId) { 3032 botId = botId || 'default'; 3033 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions'); 3034 if (questionsContainer && hasQuickQuestions(botId)) { 3035 questionsContainer.classList.add('collapsed'); 3036 questionsContainer.classList.add('has-been-collapsed'); 3037 try { 3038 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true'); 3039 sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true'); 3040 } catch (e) { 3041 // Ignore if sessionStorage is not available 3042 } 3043 } 3044} 3045 3046function expandQuickQuestions(botId) { 3047 botId = botId || 'default'; 3048 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions'); 3049 if (questionsContainer && hasQuickQuestions(botId)) { 3050 questionsContainer.classList.remove('collapsed'); 3051 try { 3052 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false'); 3053 } catch (e) { 3054 // Ignore if sessionStorage is not available 3055 } 3056 } 3057} 3058 3059function checkQuickQuestionsState(botId) { 3060 botId = botId || 'default'; 3061 if (!hasQuickQuestions(botId)) { 3062 return; // Don't do anything if no questions exist 3063 } 3064 3065 // Skip restoring collapsed state for embedded bots - they should always start expanded 3066 if (isEmbeddedBot(botId)) { 3067 return; 3068 } 3069 3070 try { 3071 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId); 3072 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId); 3073 3074 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions'); 3075 if (questionsContainer) { 3076 if (hasBeenCollapsed === 'true') { 3077 questionsContainer.classList.add('has-been-collapsed'); 3078 } 3079 if (isCollapsed === 'true') { 3080 questionsContainer.classList.add('collapsed'); 3081 } 3082 } 3083 } catch (e) { 3084 // Ignore if sessionStorage is not available 3085 } 3086} 3087 3088// Global delegation for dynamically added links as fallback 3089// Use class selector for multi-instance support 3090$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) { 3091 const $link = $(this); 3092 const messageDiv = $link.closest('.bot-message, .agent-message'); 3093 3094 // Only process bot/agent message links 3095 if (messageDiv.length > 0) { 3096 const originalHref = $link.attr('href'); 3097 3098 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) { 3099 e.preventDefault(); 3100 e.stopPropagation(); 3101 3102 // Mark as tracked 3103 $link.attr('data-tracked', 'true'); 3104 3105 // Get bot ID from the chat box context 3106 var botId = getBotIdFromElement(this); 3107 3108 // Get message context from the message div 3109 const messageText = messageDiv.text().substring(0, 200); 3110 3111 $.ajax({
3112 url: mxchatChat.ajax_url, 3113 type: 'POST', 3114 data: { 3115 action: 'mxchat_track_url_click', 3116 session_id: getChatSession(botId), 3117 url: originalHref, 3118 message_context: messageText, 3119 nonce: mxchatChat.nonce 3120 }, 3121 complete: function() { 3122 if ($link.attr('target') === '_blank' || linkTarget === '_blank') { 3123 window.open(originalHref, '_blank'); 3124 } else { 3125 window.location.href = originalHref; 3126 } 3127 } 3128 }); 3129 3130 return false; 3131 } 3132 } 3133}); 3134 3135 // ==================================== 3136 // MAIN INITIALIZATION 3137 // ==================================== 3138 3139 // Initialize all chatbot instances on the page 3140 initializeAllInstances(); 3141 3142 // Legacy initialization for single bot compatibility 3143 $('.floating-chatbot.hidden').each(function() { 3144 var botId = getBotIdFromElement(this); 3145 getElement(botId, 'floating-chatbot-button').removeClass('hidden'); 3146 }); 3147 3148 // Initialize when document is ready 3149 setFullHeight(); 3150 trackOriginatingPage(); 3151 3152 // Only load chat history if email collection is disabled 3153 if (mxchatChat.email_collection_enabled !== 'on') { 3154 // Load history for all instances 3155 $('.mxchat-chatbot-wrapper').each(function() { 3156 var botId = $(this).data('bot-id') || 'default'; 3157 loadChatHistory(botId); 3158 }); 3159 } 3160 3161 // Initialize chat visibility for all instances 3162 $('.mxchat-chatbot-wrapper').each(function() { 3163 var botId = $(this).data('bot-id') || 'default'; 3164 initializeChatVisibility(botId); 3165 }); 3166 3167 // Make functions globally available for add-ons 3168 window.hasQuickQuestions = hasQuickQuestions; 3169 window.collapseQuickQuestions = collapseQuickQuestions; 3170 window.appendMessage = appendMessage; 3171 window.appendThinkingMessage = appendThinkingMessage; 3172 window.scrollToBottom = scrollToBottom; 3173 window.scrollElementToTop = scrollElementToTop; 3174 window.replaceLastMessage = replaceLastMessage; 3175 window.callMxChat = callMxChat; 3176 window.callMxChatStream = callMxChatStream; 3177 window.shouldUseStreaming = shouldUseStreaming; 3178 window.getChatSession = getChatSession; 3179 window.getPageContext = getPageContext; 3180 window.updateStreamingMessage = updateStreamingMessage; 3181 window.MxChatInstances = MxChatInstances; 3182 window.getElement = getElement; 3183 window.getElementDOM = getElementDOM; 3184 window.getBotIdFromElement = getBotIdFromElement; 3185 3186}); // End of jQuery ready 3187 3188 3189// ==================================== 3190// GLOBAL EVENT LISTENERS (Outside jQuery) 3191// ==================================== 3192 3193// Event listener for copy button (code blocks) 3194document.addEventListener("click", (e) => { 3195 if (e.target.classList.contains("mxchat-copy-button")) { 3196 const copyButton = e.target; 3197 const codeBlock = copyButton 3198 .closest(".mxchat-code-block-container") 3199 .querySelector(".mxchat-code-block code"); 3200 3201 if (codeBlock) { 3202 // Preserve formatting using innerText 3203 navigator.clipboard.writeText(codeBlock.innerText).then(() => { 3204 copyButton.textContent = "Copied!"; 3205 copyButton.setAttribute("aria-label", "Copied to clipboard"); 3206 3207 setTimeout(() => { 3208 copyButton.textContent = "Copy"; 3209 copyButton.setAttribute("aria-label", "Copy to clipboard"); 3210 }, 2000); 3211 }); 3212 } 3213 } 3214}); 3215
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.