PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.8
MxChat – AI Chatbot & Content Generation for WordPress v3.0.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / chat-script.js

chat-script.js in MxChat – AI Chatbot & Content Generation for WordPress 3.0.8, at js/chat-script.js

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