PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.3
MxChat – AI Chatbot & Content Generation for WordPress v2.6.3
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
← All changes | js/chat-script.js +891 -1453 3.2.12.6.3 View file →
@@ -1,215 +1,12 @@
1 1 jQuery(document).ready(function($) {
2 -
3 - // Nonce refresh is deferred until first user interaction (ensureSession)
4 - // to avoid admin-ajax calls on passive page loads.
5 - var nonceRefreshed = false;
6 - function refreshNonceIfNeeded(callback) {
7 - if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
8 - if (callback) callback();
9 - return;
10 - }
11 - nonceRefreshed = true;
12 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 - if (res && res.success && res.data && res.data.nonce) {
14 - mxchatChat.nonce = res.data.nonce;
15 - }
16 - if (callback) callback();
17 - });
18 - }
19 -
2 +
20 3 // ====================================
21 - // MULTI-INSTANCE MANAGEMENT SYSTEM
22 - // ====================================
23 -
24 - // Instance registry - tracks all chatbot instances on the page
25 - const MxChatInstances = {
26 - instances: {},
27 -
28 - // Initialize an instance for a bot
29 - init: function(botId) {
30 - if (!this.instances[botId]) {
31 - // When persistence is OFF, track when this session started
32 - // so the AI only sees messages from this page load
33 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 -
35 - this.instances[botId] = {
36 - botId: botId,
37 - sessionId: null,
38 - lastSeenMessageId: '',
39 - notificationCheckInterval: null,
40 - pollingInterval: null,
41 - processedMessageIds: new Set(),
42 - activePdfFile: null,
43 - activeWordFile: null,
44 - chatHistoryLoaded: false,
45 - isStreaming: false,
46 - // Fresh context timestamp - only used when persistence is OFF
47 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
48 - };
49 - }
50 - return this.instances[botId];
51 - },
52 -
53 - // Get instance by botId
54 - get: function(botId) {
55 - return this.instances[botId] || this.init(botId);
56 - },
57 -
58 - // Get all active bot IDs
59 - getAllBotIds: function() {
60 - return Object.keys(this.instances);
61 - },
62 -
63 - // Session management per bot
64 - // Returns existing session ID from cookie or localStorage, or null if none exists.
65 - // Does NOT create a new session — use ensureSession() for that.
66 - getChatSession: function(botId) {
67 - var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 - var sessionId = getCookie(cookieName);
70 -
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 - if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
74 - }
75 -
76 - // Re-sync cookie from localStorage if cookie was lost
77 - if (sessionId && !getCookie(cookieName)) {
78 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
79 - }
80 -
81 - return sessionId || null;
82 - },
83 -
84 - // Lazy session initializer — called on first user interaction
85 - ensureSession: function(botId) {
86 - botId = botId || 'default';
87 - var instance = this.instances[botId] || this.init(botId);
88 -
89 - if (instance.sessionId) {
90 - return instance.sessionId;
91 - }
92 -
93 - // Check for existing session from cookie or localStorage
94 - var existingSession = this.getChatSession(botId);
95 -
96 - if (existingSession) {
97 - instance.sessionId = existingSession;
98 - } else {
99 - // Brand new session
100 - var newId = generateSessionId();
101 - this.setChatSession(botId, newId);
102 - instance.sessionId = newId;
103 - }
104 -
105 - // Now that we have a session, do the deferred work
106 - refreshNonceIfNeeded();
107 - trackOriginatingPage();
108 -
109 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
110 - // so we do NOT call it here to avoid a race condition.
111 -
112 - return instance.sessionId;
113 - },
114 -
115 - setChatSession: function(botId, sessionId) {
116 - var cookieName = 'mxchat_session_id_' + botId;
117 - var storageKey = 'mxchat_session_id_' + botId;
118 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
119 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
120 - if (this.instances[botId]) {
121 - this.instances[botId].sessionId = sessionId;
122 - }
123 - },
124 -
125 - resetChatSession: function(botId) {
126 - // Clear old session from localStorage before setting new one
127 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
128 - var newSessionId = generateSessionId();
129 - this.setChatSession(botId, newSessionId);
130 - var $chatBox = getElement(botId, 'chat-box');
131 - if ($chatBox.length) {
132 - $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
133 - }
134 - if (this.instances[botId]) {
135 - this.instances[botId].chatHistoryLoaded = false;
136 - this.instances[botId].processedMessageIds = new Set();
137 - }
138 - },
139 -
140 - // Silent reset — new session ID without clearing the chat UI
141 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
142 - silentResetSession: function(botId) {
143 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
144 - var newSessionId = generateSessionId();
145 - this.setChatSession(botId, newSessionId);
146 - if (this.instances[botId]) {
147 - this.instances[botId].sessionId = newSessionId;
148 - }
149 - return newSessionId;
150 - }
151 - };
152 -
153 - // ====================================
154 - // ELEMENT SELECTOR HELPERS
155 - // ====================================
156 -
157 - // Check if a specific bot has an AI theme assigned (skip inline colors)
158 - function shouldSkipInlineColors(botId) {
159 - // If global AI theme is active, skip inline colors for all bots
160 - if (mxchatChat.skip_inline_colors) {
161 - return true;
162 - }
163 - // Check if this specific bot has a theme assignment
164 - var botAssignments = mxchatChat.bot_theme_assignments || {};
165 - return botAssignments.hasOwnProperty(botId);
166 - }
167 -
168 - // Get element by ID with bot suffix - returns jQuery object
169 - function getElement(botId, elementName) {
170 - return $('#' + elementName + '-' + botId);
171 - }
172 -
173 - // Get element by ID with bot suffix - returns DOM element
174 - function getElementDOM(botId, elementName) {
175 - return document.getElementById(elementName + '-' + botId);
176 - }
177 -
178 - // Get bot ID from any element within a chatbot instance
179 - function getBotIdFromElement(element) {
180 - var $wrapper = $(element).closest('.mxchat-chatbot-wrapper');
181 - if ($wrapper.length) {
182 - return $wrapper.data('bot-id') || 'default';
183 - }
184 - // Fallback: try to find from floating container
185 - var $floating = $(element).closest('.floating-chatbot');
186 - if ($floating.length) {
187 - var id = $floating.attr('id') || '';
188 - var match = id.match(/floating-chatbot-(.+)/);
189 - if (match) return match[1];
190 - }
191 - // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
192 - var elementId = $(element).attr('id') || '';
193 - if (elementId) {
194 - // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
195 - var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+)$/);
196 - if (idMatch) return idMatch[1];
197 - }
198 - return 'default';
199 - }
200 -
201 - // Get wrapper element for a bot
202 - function getWrapper(botId) {
203 - return getElement(botId, 'mxchat-chatbot-wrapper');
204 - }
205 -
206 - // ====================================
207 4 // GLOBAL VARIABLES & CONFIGURATION
208 5 // ====================================
209 6 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
210 -
211 - // Initialize color settings (these are global as they come from PHP)
7 +
8 + // Initialize color settings
212 9 var userMessageBgColor = mxchatChat.user_message_bg_color;
213 10 var userMessageFontColor = mxchatChat.user_message_font_color;
214 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
215 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
@@ -214,64 +11,61 @@
214 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
215 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
216 13 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
217 14 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
218 -
15 +
219 16 var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
17 + let lastSeenMessageId = '';
18 + let notificationCheckInterval;
19 + let notificationBadge;
20 + var sessionId = getChatSession();
21 + let pollingInterval;
22 + let processedMessageIds = new Set();
23 + let activePdfFile = null;
24 + let activeWordFile = null;
25 + let chatHistoryLoaded = false;
220 26
221 27 // ====================================
222 - // SESSION MANAGEMENT (Legacy compatibility)
28 + // SESSION MANAGEMENT
223 29 // ====================================
30 +
31 + function getChatSession() {
32 + var sessionId = getCookie('mxchat_session_id');
224 33
34 + if (!sessionId) {
35 + sessionId = generateSessionId();
36 + setChatSession(sessionId);
37 + }
38 +
39 + return sessionId;
40 + }
41 +
42 + function setChatSession(sessionId) {
43 + // Set the cookie with a 24-hour expiration (86400 seconds)
44 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
45 + }
46 +
225 47 function getCookie(name) {
226 48 let value = "; " + document.cookie;
227 49 let parts = value.split("; " + name + "=");
228 50 if (parts.length == 2) return parts.pop().split(";").shift();
229 51 }
230 -
52 +
231 53 function generateSessionId() {
232 54 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
233 55 }
234 56
235 - // Legacy function - now delegates to instance manager
236 - function getChatSession(botId) {
237 - botId = botId || 'default';
238 - return MxChatInstances.getChatSession(botId);
57 + function resetChatSession() {
58 + // Generate a new session ID
59 + var newSessionId = generateSessionId();
60 + // Update the cookie with the new session
61 + setChatSession(newSessionId);
62 + // Clear the chat history display
63 + $('.mxchat-messages').empty();
64 + // Reset the chat history loaded flag
65 + chatHistoryLoaded = false;
239 66 }
240 67
241 - function setChatSession(sessionId, botId) {
242 - botId = botId || 'default';
243 - MxChatInstances.setChatSession(botId, sessionId);
244 - }
245 -
246 - function resetChatSession(botId) {
247 - botId = botId || 'default';
248 - MxChatInstances.resetChatSession(botId);
249 - }
250 -
251 - // ====================================
252 - // INITIALIZE ALL CHATBOT INSTANCES
253 - // ====================================
254 -
255 - function initializeAllInstances() {
256 - // Find all chatbot wrappers on the page
257 - $('.mxchat-chatbot-wrapper').each(function() {
258 - var botId = $(this).data('bot-id') || 'default';
259 - MxChatInstances.init(botId);
260 - initializeBotInstance(botId);
261 - });
262 - }
263 -
264 - function initializeBotInstance(botId) {
265 - var instance = MxChatInstances.get(botId);
266 -
267 - // Initialize quick questions state for this bot
268 - checkQuickQuestionsState(botId);
269 -
270 - // Note: Event handlers use event delegation with class selectors,
271 - // so they work automatically for all instances without per-bot setup
272 - }
273 -
274 68 // ====================================
275 69 // CONTEXTUAL AWARENESS FUNCTIONALITY
276 70 // ====================================
277 71
@@ -418,12 +212,11 @@
418 212 // CORE CHAT FUNCTIONALITY
419 213 // ====================================
420 214
421 215 // Helper functions to disable/enable chat input while waiting for response
422 -function disableChatInput(botId) {
423 - botId = botId || 'default';
424 - var chatInput = getElementDOM(botId, 'chat-input');
425 - var sendButton = getElementDOM(botId, 'send-button');
216 +function disableChatInput() {
217 + var chatInput = document.getElementById('chat-input');
218 + var sendButton = document.getElementById('send-button');
426 219 if (chatInput) {
427 220 chatInput.disabled = true;
428 221 chatInput.style.opacity = '0.6';
429 222 }
@@ -433,12 +226,11 @@
433 226 sendButton.style.pointerEvents = 'none';
434 227 }
435 228 }
436 229
437 -function enableChatInput(botId) {
438 - botId = botId || 'default';
439 - var chatInput = getElementDOM(botId, 'chat-input');
440 - var sendButton = getElementDOM(botId, 'send-button');
230 +function enableChatInput() {
231 + var chatInput = document.getElementById('chat-input');
232 + var sendButton = document.getElementById('send-button');
441 233 if (chatInput) {
442 234 chatInput.disabled = false;
443 235 chatInput.style.opacity = '1';
444 236 chatInput.focus();
@@ -450,13 +242,10 @@
450 242 }
451 243 }
452 244
453 245 // Update your existing sendMessage function
454 -function sendMessage(botId) {
455 - botId = botId || 'default';
456 - MxChatInstances.ensureSession(botId);
457 - var $chatInput = getElement(botId, 'chat-input');
458 - var message = $chatInput.val();
246 +function sendMessage() {
247 + var message = $('#chat-input').val();
459 248
460 249 // ADD PROMPT HOOK HERE
461 250 if (typeof customMxChatFilter === 'function') {
462 251 message = customMxChatFilter(message, "prompt");
@@ -462,77 +251,66 @@
462 251 message = customMxChatFilter(message, "prompt");
463 252 }
464 253
465 254 if (message) {
466 - // Don't disable input in live agent mode - let users chat freely
467 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
468 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
469 - if (!isAgentMode) {
470 - disableChatInput(botId);
471 - }
255 + // Disable input while waiting for response
256 + disableChatInput();
472 257
473 - appendMessage("user", message, '', [], false, botId);
474 - $chatInput.val('');
475 - $chatInput.css('height', 'auto');
258 + appendMessage("user", message);
259 + $('#chat-input').val('');
260 + $('#chat-input').css('height', 'auto');
476 261
477 - if (hasQuickQuestions(botId)) {
478 - collapseQuickQuestions(botId);
262 + if (hasQuickQuestions()) {
263 + collapseQuickQuestions();
479 264 }
480 - appendThinkingMessage(botId);
481 - scrollToBottom(botId);
265 + appendThinkingMessage();
266 + scrollToBottom();
482 267
483 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
268 + const currentModel = mxchatChat.model || 'gpt-4o';
484 269
485 270 // Check if streaming is enabled AND supported for this model
486 271 if (shouldUseStreaming(currentModel)) {
487 272 callMxChatStream(message, function(response) {
488 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
489 - }, botId);
273 + $('.bot-message.temporary-message').removeClass('temporary-message');
274 + });
490 275 } else {
491 276 callMxChat(message, function(response) {
492 - replaceLastMessage("bot", response, '', [], botId);
493 - }, botId);
277 + replaceLastMessage("bot", response);
278 + });
494 279 }
495 280 }
496 281 }
497 282
498 283 // Update your existing sendMessageToChatbot function
499 -function sendMessageToChatbot(message, botId) {
500 - botId = botId || 'default';
501 - MxChatInstances.ensureSession(botId);
502 -
284 +function sendMessageToChatbot(message) {
503 285 // ADD PROMPT HOOK HERE
504 286 if (typeof customMxChatFilter === 'function') {
505 287 message = customMxChatFilter(message, "prompt");
506 288 }
507 289
508 - // Don't disable input in live agent mode - let users chat freely
509 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
510 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
511 - if (!isAgentMode) {
512 - disableChatInput(botId);
513 - }
290 + // Disable input while waiting for response
291 + disableChatInput();
514 292
515 - var sessionId = getChatSession(botId);
293 + var sessionId = getChatSession();
516 294
517 - if (hasQuickQuestions(botId)) {
518 - collapseQuickQuestions(botId);
295 + if (hasQuickQuestions()) {
296 + collapseQuickQuestions();
519 297 }
520 - appendThinkingMessage(botId);
521 - scrollToBottom(botId);
298 + appendThinkingMessage();
299 + scrollToBottom();
522 300
523 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
301 + const currentModel = mxchatChat.model || 'gpt-4o';
524 302
525 303 // Check if streaming is enabled AND supported for this model
526 304 if (shouldUseStreaming(currentModel)) {
527 305 callMxChatStream(message, function(response) {
528 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
529 - }, botId);
306 + $('.bot-message.temporary-message').removeClass('temporary-message');
307 + });
530 308 } else {
531 309 callMxChat(message, function(response) {
532 - getElement(botId, 'chat-box').find('.temporary-message').remove();
533 - replaceLastMessage("bot", response, '', [], botId);
534 - }, botId);
310 + $('.temporary-message').remove();
311 + replaceLastMessage("bot", response);
312 + });
535 313 }
536 314 }
537 315
538 316 // Updated shouldUseStreaming function with debugging
@@ -575,43 +353,33 @@
575 353 updateChatModeIndicator('agent');
576 354 }
577 355 }
578 356
579 -// Function to get bot ID from any element or wrapper
580 -// If element is provided, finds the bot ID from its wrapper
581 -// If no element, returns 'default' (for backward compatibility)
582 -function getMxChatBotId(element) {
583 - if (element) {
584 - return getBotIdFromElement(element);
585 - }
586 - // Fallback: find first chatbot wrapper on page
587 - const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper');
357 +//Function to get bot ID from the chatbot wrapper
358 +function getMxChatBotId() {
359 + const chatbotWrapper = document.getElementById('mxchat-chatbot-wrapper');
588 360 return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
589 361 }
590 362
591 -function callMxChat(message, callback, botId) {
592 - botId = botId || getMxChatBotId();
593 -
363 +function callMxChat(message, callback) {
594 364 // Store the message in case we need to retry after session reset
595 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
365 + $('.mxchat-input-holder textarea').data('pending-message', message);
596 366
597 367 // Get page context if contextual awareness is enabled
598 368 const pageContext = getPageContext();
599 369
600 - // Get instance for session start timestamp (used when persistence is OFF)
601 - var instance = MxChatInstances.get(botId);
370 + // Get bot ID
371 + const botId = getMxChatBotId();
602 372
603 373 // Prepare AJAX data
604 374 const ajaxData = {
605 375 action: 'mxchat_handle_chat_request',
606 376 message: message,
607 - session_id: getChatSession(botId),
377 + session_id: getChatSession(),
608 378 nonce: mxchatChat.nonce,
609 379 current_page_url: window.location.href,
610 380 current_page_title: document.title,
611 - bot_id: botId,
612 - // Pass session start timestamp so AI context matches what user sees
613 - session_start_timestamp: instance.sessionStartTimestamp || 0
381 + bot_id: botId // Include bot ID
614 382 };
615 383
616 384 // Add page context if available
617 385 if (pageContext) {
@@ -636,14 +404,14 @@
636 404 data: ajaxData,
637 405 success: function(response) {
638 406 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
639 407 if (response.chat_mode) {
640 - updateChatModeIndicator(response.chat_mode, botId);
408 + updateChatModeIndicator(response.chat_mode);
641 409 }
642 410
643 411 // Also check in data property if response is wrapped
644 412 if (response.data && response.data.chat_mode) {
645 - updateChatModeIndicator(response.data.chat_mode, botId);
413 + updateChatModeIndicator(response.data.chat_mode);
646 414 }
647 415
648 416 // SECURITY FIX: Check for errors FIRST before checking for success
649 417 // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
@@ -667,24 +435,31 @@
667 435 errorMessage = "An error occurred. Please try again or contact support.";
668 436 }
669 437
670 438 // Handle session reset action (IP changed, session expired, etc.)
671 - // Silent reset — keep chat UI intact, just get a new session and retry
672 439 if (response.data && response.data.action === 'reset_session') {
673 - MxChatInstances.silentResetSession(botId);
674 - // Re-send the original message with the new session (user message is already displayed)
675 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
440 + // Clear the old session and generate a new one
441 + resetChatSession();
442 + // Remove the temporary loading message
443 + $('.bot-message.temporary-message').remove();
444 + // Re-send the original message with the new session
445 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
676 446 if (originalMessage) {
677 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
678 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
447 + $('.mxchat-input-holder textarea').data('pending-message', null);
448 + // Re-add the user message and thinking indicator
449 + appendMessage("user", originalMessage);
450 + appendThinkingMessage();
451 + scrollToBottom();
452 + // Determine whether to use streaming
453 + const currentModel = mxchatChat.model || 'gpt-4o';
679 454 if (shouldUseStreaming(currentModel)) {
680 455 callMxChatStream(originalMessage, function(response) {
681 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
682 - }, botId);
456 + $('.bot-message.temporary-message').removeClass('temporary-message');
457 + });
683 458 } else {
684 459 callMxChat(originalMessage, function(response) {
685 - replaceLastMessage("bot", response, '', [], botId);
686 - }, botId);
460 + replaceLastMessage("bot", response);
461 + });
687 462 }
688 463 }
689 464 return;
690 465 }
@@ -697,9 +472,9 @@
697 472 // For admin users, show more technical details including error code
698 473 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
699 474 }
700 475
701 - replaceLastMessage("bot", displayMessage, '', [], botId);
476 + replaceLastMessage("bot", displayMessage);
702 477 return; // Exit early for errors
703 478 }
704 479
705 480 // NOW check if this is a successful response by looking for text, html, or message fields
@@ -715,17 +490,16 @@
715 490 let responseMessage = response.message || '';
716 491
717 492 // Add PDF filename handling
718 493 if (response.data && response.data.filename) {
719 - showActivePdf(response.data.filename, botId);
720 - var instance = MxChatInstances.get(botId);
721 - instance.activePdfFile = response.data.filename;
494 + showActivePdf(response.data.filename);
495 + activePdfFile = response.data.filename;
722 496 }
723 497
724 498 // Add redirect check here
725 499 if (response.redirect_url) {
726 500 if (responseText) {
727 - replaceLastMessage("bot", responseText, '', [], botId);
501 + replaceLastMessage("bot", responseText);
728 502 }
729 503 setTimeout(() => {
730 504 window.location.href = response.redirect_url;
731 505 }, 1500);
@@ -733,11 +507,9 @@
733 507 }
734 508
735 509 // Check for live agent response
736 510 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
737 - removeThinkingDots(botId);
738 - updateChatModeIndicator('agent', botId);
739 - enableChatInput(botId);
511 + updateChatModeIndicator('agent');
740 512 return;
741 513 }
742 514
743 515 // Handle the message and show notification if chat is hidden
@@ -752,36 +524,30 @@
752 524 }
753 525
754 526 // Update the messages as before
755 527 if (responseText && responseHtml) {
756 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
528 + replaceLastMessage("bot", responseText, responseHtml);
757 529 } else if (responseText) {
758 - replaceLastMessage("bot", responseText, '', [], botId);
530 + replaceLastMessage("bot", responseText);
759 531 } else if (responseHtml) {
760 - replaceLastMessage("bot", "", responseHtml, [], botId);
532 + replaceLastMessage("bot", "", responseHtml);
761 533 } else if (responseMessage) {
762 - replaceLastMessage("bot", responseMessage, '', [], botId);
534 + replaceLastMessage("bot", responseMessage);
763 535 }
764 536
765 537 // Check if chat is hidden and show notification
766 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
767 - if ($floatingChatbot.hasClass('hidden')) {
768 - var $badge = getElement(botId, 'chat-notification-badge');
769 - if ($badge.length) {
770 - $badge.show();
538 + if ($('#floating-chatbot').hasClass('hidden')) {
539 + const badge = $('#chat-notification-badge');
540 + if (badge.length) {
541 + badge.show();
771 542 }
772 543 }
773 544 } else {
774 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
775 - if (response.vectorstore_error) {
776 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
777 - }
778 - replaceLastMessage("bot", emptyMsg, '', [], botId);
545 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
779 546 }
780 547
781 548 if (response.message_id) {
782 - var instance = MxChatInstances.get(botId);
783 - instance.lastSeenMessageId = response.message_id;
549 + lastSeenMessageId = response.message_id;
784 550 }
785 551
786 552 return;
787 553 }
@@ -786,9 +552,9 @@
786 552 return;
787 553 }
788 554
789 555 // Fallback for truly unexpected response formats
790 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
556 + replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.");
791 557 },
792 558 error: function(xhr, status, error) {
793 559 let errorMessage = "An unexpected error occurred.";
794 560
@@ -815,42 +581,38 @@
815 581 errorMessage = "Server error: The server encountered an issue. Please try again later.";
816 582 }
817 583 }
818 584
819 - replaceLastMessage("bot", errorMessage, '', [], botId);
585 + replaceLastMessage("bot", errorMessage);
820 586 }
821 587 });
822 588 }
823 589
824 -function callMxChatStream(message, callback, botId) {
825 - botId = botId || getMxChatBotId();
826 -
590 +function callMxChatStream(message, callback) {
827 591 // Store the message in case we need to retry after session reset
828 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
592 + $('.mxchat-input-holder textarea').data('pending-message', message);
829 593
830 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
594 + const currentModel = mxchatChat.model || 'gpt-4o';
831 595 if (!isStreamingSupported(currentModel)) {
832 - callMxChat(message, callback, botId);
596 + callMxChat(message, callback);
833 597 return;
834 598 }
835 599
836 600 // Get page context if contextual awareness is enabled
837 601 const pageContext = getPageContext();
602 +
603 + // Get bot ID
604 + const botId = getMxChatBotId();
838 605
839 - // Get instance for session start timestamp (used when persistence is OFF)
840 - var instance = MxChatInstances.get(botId);
841 -
842 606 const formData = new FormData();
843 607 formData.append('action', 'mxchat_stream_chat');
844 608 formData.append('message', message);
845 - formData.append('session_id', getChatSession(botId));
609 + formData.append('session_id', getChatSession());
846 610 formData.append('nonce', mxchatChat.nonce);
847 611 formData.append('current_page_url', window.location.href);
848 612 formData.append('current_page_title', document.title);
849 - formData.append('bot_id', botId);
850 - // Pass session start timestamp so AI context matches what user sees
851 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
852 -
613 + formData.append('bot_id', botId); // Include bot ID
614 +
853 615 // Add page context if available
854 616 if (pageContext) {
855 617 formData.append('page_context', JSON.stringify(pageContext));
856 618 }
@@ -893,9 +655,9 @@
893 655 if (contentType && contentType.includes('application/json')) {
894 656 return responseClone.json().then(data => {
895 657 // IMMEDIATE CHAT MODE UPDATE for JSON response
896 658 if (data.chat_mode) {
897 - updateChatModeIndicator(data.chat_mode, botId);
659 + updateChatModeIndicator(data.chat_mode);
898 660 }
899 661
900 662 // Check for testing panel
901 663 if (window.mxchatTestPanelInstance && data.testing_data) {
@@ -902,9 +664,9 @@
902 664 window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
903 665 }
904 666
905 667 // Handle the JSON response directly
906 - handleNonStreamResponse(data, callback, botId);
668 + handleNonStreamResponse(data, callback);
907 669 return Promise.resolve(); // Prevent further processing
908 670 });
909 671 }
910 672
@@ -922,29 +684,26 @@
922 684 responseClone.text().then(text => {
923 685 try {
924 686 const data = JSON.parse(text);
925 687 if (data.text || data.message || data.html) {
926 - handleNonStreamResponse(data, callback, botId);
688 + handleNonStreamResponse(data, callback);
927 689 } else {
928 690 // No valid data, fall back to regular call
929 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
930 - callMxChat(message, callback, botId);
691 + $('.bot-message.temporary-message').remove();
692 + callMxChat(message, callback);
931 693 }
932 694 } catch (e) {
933 695 // Could not parse, fall back to regular call
934 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
935 - callMxChat(message, callback, botId);
696 + $('.bot-message.temporary-message').remove();
697 + callMxChat(message, callback);
936 698 }
937 699 }).catch(() => {
938 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
939 - callMxChat(message, callback, botId);
700 + $('.bot-message.temporary-message').remove();
701 + callMxChat(message, callback);
940 702 });
941 703 return;
942 704 }
943 -
944 - // Re-enable chat input when stream ends with content
945 - enableChatInput(botId);
946 -
705 +
947 706 if (callback) {
948 707 callback(accumulatedContent);
949 708 }
950 709 return;
@@ -959,15 +718,15 @@
959 718 const data = line.substring(6);
960 719
961 720 if (data === '[DONE]') {
962 721 if (!accumulatedContent) {
963 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
964 - callMxChat(message, callback, botId);
722 + $('.bot-message.temporary-message').remove();
723 + callMxChat(message, callback);
965 724 return;
966 725 }
967 726
968 727 // Re-enable chat input after streaming completes
969 - enableChatInput(botId);
728 + enableChatInput();
970 729
971 730 if (callback) {
972 731 callback(accumulatedContent);
973 732 }
@@ -978,9 +737,9 @@
978 737 const json = JSON.parse(data);
979 738
980 739 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
981 740 if (json.chat_mode) {
982 - updateChatModeIndicator(json.chat_mode, botId);
741 + updateChatModeIndicator(json.chat_mode);
983 742 }
984 743
985 744 // Handle testing data
986 745 if (json.testing_data && !testingDataReceived) {
@@ -992,13 +751,13 @@
992 751 // Handle content streaming
993 752 else if (json.content) {
994 753 streamingStarted = true;
995 754 accumulatedContent += json.content;
996 - updateStreamingMessage(accumulatedContent, botId);
755 + updateStreamingMessage(accumulatedContent);
997 756 }
998 757 // Handle complete response in stream (fallback response)
999 758 else if (json.text || json.message || json.html) {
1000 - handleNonStreamResponse(json, callback, botId);
759 + handleNonStreamResponse(json, callback);
1001 760 return;
1002 761 }
1003 762 // Handle errors
1004 763 else if (json.error) {
@@ -1007,12 +766,12 @@
1007 766 let errorMessage = json.error_message || json.message || json.text ||
1008 767 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1009 768
1010 769 // Re-enable chat input on error
1011 - enableChatInput(botId);
770 + enableChatInput();
1012 771
1013 772 // Display the error directly in the chat
1014 - replaceLastMessage("bot", errorMessage, '', [], botId);
773 + replaceLastMessage("bot", errorMessage);
1015 774
1016 775 if (callback) {
1017 776 callback(errorMessage);
1018 777 }
@@ -1025,10 +784,10 @@
1025 784 }
1026 785
1027 786 processStream();
1028 787 }).catch(streamError => {
1029 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1030 - callMxChat(message, callback, botId);
788 + $('.bot-message.temporary-message').remove();
789 + callMxChat(message, callback);
1031 790 });
1032 791 }
1033 792
1034 793 processStream();
@@ -1037,32 +796,30 @@
1037 796 // Check if we have server error data with chat mode
1038 797 if (error && error.isServerError && error.data) {
1039 798 // Check for chat mode in error data
1040 799 if (error.data.chat_mode) {
1041 - updateChatModeIndicator(error.data.chat_mode, botId);
800 + updateChatModeIndicator(error.data.chat_mode);
1042 801 }
1043 802
1044 - handleNonStreamResponse(error.data, callback, botId);
803 + handleNonStreamResponse(error.data, callback);
1045 804 } else {
1046 805 // Only fall back to regular call if we don't have any response data
1047 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1048 - callMxChat(message, callback, botId);
806 + $('.bot-message.temporary-message').remove();
807 + callMxChat(message, callback);
1049 808 }
1050 809 });
1051 810 }
1052 811
1053 812 // Helper function to handle non-streaming responses
1054 -function handleNonStreamResponse(data, callback, botId) {
1055 - botId = botId || 'default';
1056 -
813 +function handleNonStreamResponse(data, callback) {
1057 814 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1058 815 if (data.chat_mode) {
1059 - updateChatModeIndicator(data.chat_mode, botId);
816 + updateChatModeIndicator(data.chat_mode);
1060 817 }
1061 818
1062 819 // Also check in data property if response is wrapped
1063 820 if (data.data && data.data.chat_mode) {
1064 - updateChatModeIndicator(data.data.chat_mode, botId);
821 + updateChatModeIndicator(data.data.chat_mode);
1065 822 }
1066 823
1067 824 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1068 825 // This prevents a visual gap between thinking dots disappearing and content appearing
@@ -1087,20 +844,25 @@
1087 844 errorMessage = "An error occurred. Please try again or contact support.";
1088 845 }
1089 846
1090 847 // Handle session reset action (IP changed, session expired, etc.)
1091 - // Silent reset — keep chat UI intact, just get a new session and retry
1092 848 if (data.data && data.data.action === 'reset_session') {
1093 - MxChatInstances.silentResetSession(botId);
1094 - // Re-send the original message with the new session (user message is already displayed)
1095 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
849 + // Clear the old session and generate a new one
850 + resetChatSession();
851 + // Re-send the original message with the new session
852 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
1096 853 if (originalMessage) {
1097 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1098 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
854 + $('.mxchat-input-holder textarea').data('pending-message', null);
855 + // Re-add the user message and thinking indicator
856 + appendMessage("user", originalMessage);
857 + appendThinkingMessage();
858 + scrollToBottom();
859 + // Determine whether to use streaming
860 + const currentModel = mxchatChat.model || 'gpt-4o';
1099 861 if (shouldUseStreaming(currentModel)) {
1100 - callMxChatStream(originalMessage, callback, botId);
862 + callMxChatStream(originalMessage, callback);
1101 863 } else {
1102 - callMxChat(originalMessage, callback, botId);
864 + callMxChat(originalMessage, callback);
1103 865 }
1104 866 }
1105 867 return;
1106 868 }
@@ -1110,9 +872,9 @@
1110 872 if (mxchatChat.is_admin) {
1111 873 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1112 874 }
1113 875
1114 - replaceLastMessage("bot", displayMessage, '', [], botId);
876 + replaceLastMessage("bot", displayMessage);
1115 877
1116 878 if (callback) {
1117 879 callback('');
1118 880 }
@@ -1118,22 +880,8 @@
1118 880 }
1119 881 return; // Exit early for errors
1120 882 }
1121 883
1122 - // Check for live agent response
1123 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1124 - removeThinkingDots(botId);
1125 - // Also remove any leftover bot-message that lost its temporary-message class
1126 - var $chatBox = getElement(botId, 'chat-box');
1127 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1128 - updateChatModeIndicator('agent', botId);
1129 - enableChatInput(botId);
1130 - if (callback) {
1131 - callback('');
1132 - }
1133 - return;
1134 - }
1135 -
1136 884 // Handle different response formats
1137 885 if (data.text || data.html || data.message) {
1138 886
1139 887 // Apply response hooks
@@ -1145,23 +893,22 @@
1145 893 }
1146 894
1147 895 // Display the response
1148 896 if (data.text && data.html) {
1149 - replaceLastMessage("bot", data.text, data.html, [], botId);
897 + replaceLastMessage("bot", data.text, data.html);
1150 898 } else if (data.text) {
1151 - replaceLastMessage("bot", data.text, '', [], botId);
899 + replaceLastMessage("bot", data.text);
1152 900 } else if (data.html) {
1153 - replaceLastMessage("bot", "", data.html, [], botId);
901 + replaceLastMessage("bot", "", data.html);
1154 902 } else if (data.message) {
1155 - replaceLastMessage("bot", data.message, '', [], botId);
903 + replaceLastMessage("bot", data.message);
1156 904 }
1157 905 }
1158 906
1159 907 // Handle other response properties
1160 908 if (data.data && data.data.filename) {
1161 - showActivePdf(data.data.filename, botId);
1162 - var instance = MxChatInstances.get(botId);
1163 - instance.activePdfFile = data.data.filename;
909 + showActivePdf(data.data.filename);
910 + activePdfFile = data.data.filename;
1164 911 }
1165 912
1166 913 if (data.redirect_url) {
1167 914 setTimeout(() => {
@@ -1169,9 +916,9 @@
1169 916 }, 1500);
1170 917 }
1171 918
1172 919 // Ensure chat input is re-enabled (safety net for edge cases)
1173 - enableChatInput(botId);
920 + enableChatInput();
1174 921
1175 922 if (callback) {
1176 923 callback(data.text || data.message || '');
1177 924 }
@@ -1177,22 +924,21 @@
1177 924 }
1178 925 }
1179 926
1180 927 // Enhanced updateChatModeIndicator function for immediate DOM updates
1181 -function updateChatModeIndicator(mode, botId) {
1182 - botId = botId || 'default';
1183 - const indicator = getElementDOM(botId, 'chat-mode-indicator');
928 +function updateChatModeIndicator(mode) {
929 + const indicator = document.getElementById('chat-mode-indicator');
1184 930 if (indicator) {
1185 931 const oldText = indicator.textContent;
1186 932
1187 933 if (mode === 'agent') {
1188 934 indicator.textContent = 'Live Agent';
1189 - startPolling(botId);
935 + startPolling();
1190 936 } else {
1191 937 // Everything else is AI mode
1192 938 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1193 939 indicator.textContent = customAiText;
1194 - stopPolling(botId);
940 + stopPolling();
1195 941 }
1196 942
1197 943 // Force immediate DOM update and reflow
1198 944 if (oldText !== indicator.textContent) {
@@ -1214,21 +960,18 @@
1214 960 }
1215 961 }
1216 962
1217 963 // Function to update message during streaming
1218 -function updateStreamingMessage(content, botId) {
1219 - botId = botId || 'default';
1220 -
964 +function updateStreamingMessage(content) {
1221 965 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1222 966 if (typeof customMxChatFilter === 'function') {
1223 967 content = customMxChatFilter(content, "response");
1224 968 }
1225 -
969 +
1226 970 const formattedContent = linkify(content);
1227 971
1228 - // Find the temporary message in this bot's chat box
1229 - var $chatBox = getElement(botId, 'chat-box');
1230 - const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
972 + // Find the temporary message
973 + const tempMessage = $('.bot-message.temporary-message').last();
1231 974
1232 975 if (tempMessage.length) {
1233 976 // Update existing message
1234 977 tempMessage.html(formattedContent);
@@ -1233,9 +976,9 @@
1233 976 // Update existing message
1234 977 tempMessage.html(formattedContent);
1235 978 } else {
1236 979 // Create new temporary message if it doesn't exist
1237 - appendMessage("bot", content, '', [], true, botId);
980 + appendMessage("bot", content, '', [], true);
1238 981 }
1239 982 }
1240 983
1241 984 function isStreamingSupported(model) {
@@ -1254,33 +997,24 @@
1254 997 return isSupported;
1255 998 }
1256 999
1257 1000 // Update the event handlers to use the correct function names (using event delegation)
1258 -// Use class-based selectors for multi-instance support
1259 -$(document).on('click', '.send-button', function() {
1260 - var botId = getBotIdFromElement(this);
1261 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1262 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1263 - disableChatInput(botId);
1264 - }
1265 - sendMessage(botId);
1001 +$(document).on('click', '#send-button', function() {
1002 + disableChatInput();
1003 + sendMessage();
1266 1004 });
1267 1005
1268 1006 // Override enter key handler (using event delegation)
1269 -$(document).on('keypress', '.chat-input', function(e) {
1007 +$(document).on('keypress', '#chat-input', function(e) {
1270 1008 if (e.which == 13 && !e.shiftKey) {
1271 1009 e.preventDefault();
1272 - var botId = getBotIdFromElement(this);
1273 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1274 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1275 - disableChatInput(botId);
1276 - }
1277 - sendMessage(botId);
1010 + disableChatInput();
1011 + sendMessage();
1278 1012 }
1279 1013 });
1280 1014
1281 1015
1282 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1016 +function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
1283 1017 try {
1284 1018 // Determine styles based on sender type
1285 1019 let messageClass, bgColor, fontColor;
1286 1020
@@ -1301,29 +1035,25 @@
1301 1035 }
1302 1036
1303 1037 const messageDiv = $('<div>')
1304 1038 .addClass(messageClass)
1305 - .attr('dir', 'auto');
1306 -
1307 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1308 - var skipColors = shouldSkipInlineColors(botId);
1309 - if (skipColors) {
1310 - messageDiv.css({
1311 - 'margin-bottom': '1em'
1312 - });
1313 - } else {
1314 - messageDiv.css({
1039 + .attr('dir', 'auto')
1040 + .css({
1315 1041 'background': bgColor,
1316 1042 'color': fontColor,
1317 1043 'margin-bottom': '1em'
1318 1044 });
1045 +
1046 + // Process the message content based on sender
1047 + let fullMessage;
1048 + if (sender === "user") {
1049 + // For user messages, apply linkify after sanitization
1050 + fullMessage = linkify(messageText);
1051 + } else {
1052 + // For bot/agent messages, preserve HTML
1053 + fullMessage = messageText;
1319 1054 }
1320 1055
1321 - // Process the message content - always run linkify to convert markdown
1322 - // links and format text. linkify() handles existing HTML safely via
1323 - // negative lookaheads that skip URLs already inside <a> tags.
1324 - let fullMessage = linkify(messageText);
1325 -
1326 1056 // Add images if provided
1327 1057 if (images && images.length > 0) {
1328 1058 fullMessage += '<div class="image-gallery" dir="auto">';
1329 1059 images.forEach(img => {
@@ -1329,9 +1059,9 @@
1329 1059 images.forEach(img => {
1330 1060 const safeTitle = sanitizeUserInput(img.title);
1331 1061 const safeUrl = encodeURI(img.image_url);
1332 1062 const safeThumbnail = encodeURI(img.thumbnail_url);
1333 -
1063 +
1334 1064 fullMessage += `
1335 1065 <div style="margin-bottom: 10px;">
1336 1066 <strong>${safeTitle}</strong><br>
1337 1067 <a href="${safeUrl}" target="_blank">
@@ -1357,28 +1087,25 @@
1357 1087 if (isTemporary) {
1358 1088 messageDiv.addClass('temporary-message');
1359 1089 }
1360 1090
1361 - // Append to the correct chatbot instance's chat-box
1362 - var $chatBox = getElement(botId, 'chat-box');
1363 - messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1091 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1364 1092 // FIXED: Use event delegation for link tracking
1365 1093 if (sender === "bot" || sender === "agent") {
1366 - attachLinkTracking(messageDiv, messageText, botId);
1094 + attachLinkTracking(messageDiv, messageText);
1367 1095 }
1368 -
1096 +
1369 1097 if (sender === "bot") {
1370 - const lastUserMessage = $chatBox.find('.user-message').last();
1098 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1371 1099 if (lastUserMessage.length) {
1372 - scrollElementToTop(lastUserMessage, botId);
1100 + scrollElementToTop(lastUserMessage);
1373 1101 }
1374 1102 }
1375 1103 });
1376 1104
1377 1105 if (messageText.id) {
1378 - var instance = MxChatInstances.get(botId);
1379 - instance.lastSeenMessageId = messageText.id;
1380 - hideNotification(botId);
1106 + lastSeenMessageId = messageText.id;
1107 + hideNotification();
1381 1108 }
1382 1109 } catch (error) {
1383 1110 // Error rendering message - silently continue
1384 1111 }
@@ -1384,35 +1111,34 @@
1384 1111 }
1385 1112 }
1386 1113
1387 1114 // Helper function to attach link tracking with proper event handling
1388 -function attachLinkTracking(messageDiv, messageText, botId) {
1389 - botId = botId || 'default';
1115 +function attachLinkTracking(messageDiv, messageText) {
1390 1116 // Use a slight delay to ensure DOM is ready
1391 1117 setTimeout(function() {
1392 1118 const links = messageDiv.find('a[href]').not('[data-tracked]');
1393 -
1119 +
1394 1120 links.each(function() {
1395 1121 const $link = $(this);
1396 1122 const originalHref = $link.attr('href');
1397 -
1123 +
1398 1124 // Mark as tracked to avoid duplicate handlers
1399 1125 $link.attr('data-tracked', 'true');
1400 -
1126 +
1401 1127 // Only track external URLs
1402 1128 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1403 1129 // Remove any existing click handlers first
1404 1130 $link.off('click.tracking');
1405 -
1131 +
1406 1132 // Add new click handler with namespace
1407 1133 $link.on('click.tracking', function(e) {
1408 1134 e.preventDefault();
1409 1135 e.stopPropagation();
1410 -
1411 - const messageContext = typeof messageText === 'string'
1412 - ? messageText.substring(0, 200)
1136 +
1137 + const messageContext = typeof messageText === 'string'
1138 + ? messageText.substring(0, 200)
1413 1139 : '';
1414 -
1140 +
1415 1141 // Track the click
1416 1142 $.ajax({
1417 1143 url: mxchatChat.ajax_url,
1418 1144 type: 'POST',
@@ -1417,9 +1143,9 @@
1417 1143 url: mxchatChat.ajax_url,
1418 1144 type: 'POST',
1419 1145 data: {
1420 1146 action: 'mxchat_track_url_click',
1421 - session_id: getChatSession(botId),
1147 + session_id: getChatSession(),
1422 1148 url: originalHref,
1423 1149 message_context: messageContext,
1424 1150 nonce: mxchatChat.nonce
1425 1151 },
@@ -1431,9 +1157,9 @@
1431 1157 window.location.href = originalHref;
1432 1158 }
1433 1159 }
1434 1160 });
1435 -
1161 +
1436 1162 return false; // Extra insurance to prevent default
1437 1163 });
1438 1164 }
1439 1165 });
@@ -1439,12 +1165,11 @@
1439 1165 });
1440 1166 }, 100); // Small delay to ensure DOM is ready
1441 1167 }
1442 1168
1443 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1169 +function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1444 1170 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1445 - var $chatBox = getElement(botId, 'chat-box');
1446 - var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1171 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1447 1172
1448 1173 // Determine styles
1449 1174 let bgColor, fontColor;
1450 1175 if (sender === "user") {
@@ -1457,13 +1182,27 @@
1457 1182 bgColor = botMessageBgColor;
1458 1183 fontColor = botMessageFontColor;
1459 1184 }
1460 1185
1461 - // Always run linkify to convert markdown links and format text.
1462 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1463 - // to avoid double-processing URLs that are already inside <a> tags).
1464 - var fullMessage = linkify(responseText);
1465 -
1186 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1187 + // This prevents double-processing of URLs that are already formatted as HTML
1188 + var fullMessage;
1189 + if (sender === "user") {
1190 + // Always linkify user messages (they're plain text)
1191 + fullMessage = linkify(responseText);
1192 + } else {
1193 + // For bot/agent messages, check if HTML already exists
1194 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1195 + responseText.includes('<img') || responseText.includes('<div') ||
1196 + responseText.includes('<p>') || responseText.includes('<br>')) {
1197 + // Response already has HTML, don't process it
1198 + fullMessage = responseText;
1199 + } else {
1200 + // Plain text response, apply linkify
1201 + fullMessage = linkify(responseText);
1202 + }
1203 + }
1204 +
1466 1205 if (responseHtml) {
1467 1206 // Only add line breaks if there's actual text content before the HTML
1468 1207 if (fullMessage && fullMessage.trim()) {
1469 1208 fullMessage += '<br><br>' + responseHtml;
@@ -1491,87 +1230,62 @@
1491 1230 lastMessageDiv
1492 1231 .html(fullMessage)
1493 1232 .removeClass('bot-message user-message temporary-message')
1494 1233 .addClass(messageClass)
1495 - .attr('dir', 'auto');
1496 -
1497 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1498 - var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1499 - if (!skipColors) {
1500 - lastMessageDiv.css({
1234 + .attr('dir', 'auto')
1235 + .css({
1501 1236 'background-color': bgColor,
1502 1237 'color': fontColor,
1503 1238 });
1504 - }
1505 1239
1506 1240 // Handle link tracking and scroll
1507 1241 if (sender === "bot" || sender === "agent") {
1508 - attachLinkTracking(lastMessageDiv, responseText, botId);
1242 + attachLinkTracking(lastMessageDiv, responseText);
1509 1243
1510 - const lastUserMessage = $chatBox.find('.user-message').last();
1244 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1511 1245 if (lastUserMessage.length) {
1512 - scrollElementToTop(lastUserMessage, botId);
1246 + scrollElementToTop(lastUserMessage);
1513 1247 }
1514 1248 // Show notification if chat is hidden
1515 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1516 - if ($floatingChatbot.hasClass('hidden')) {
1517 - showNotification(botId);
1249 + if ($('#floating-chatbot').hasClass('hidden')) {
1250 + showNotification();
1518 1251 }
1519 1252 }
1520 1253
1521 1254 // Re-enable chat input after response is displayed
1522 - enableChatInput(botId);
1255 + enableChatInput();
1523 1256 } else {
1524 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1257 + appendMessage(sender, responseText, responseHtml, images);
1525 1258 // Re-enable chat input after response is displayed
1526 - enableChatInput(botId);
1259 + enableChatInput();
1527 1260 }
1528 1261 }
1529 1262
1530 1263
1531 - function appendThinkingMessage(botId) {
1532 - botId = botId || 'default';
1264 + function appendThinkingMessage() {
1265 + // Remove any existing thinking dots first
1266 + $('.thinking-dots').remove();
1533 1267
1534 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1535 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1536 - if (indicator && indicator.textContent === 'Live Agent') {
1537 - return;
1538 - }
1539 -
1540 - var $chatBox = getElement(botId, 'chat-box');
1541 -
1542 - // Remove any existing thinking dots in this bot's chat first
1543 - $chatBox.find('.thinking-dots').remove();
1544 -
1545 - // Check if we should skip inline colors (AI theme is active)
1546 - var skipColors = shouldSkipInlineColors(botId);
1547 -
1548 1268 // Retrieve the bot message font color and background color
1549 1269 var botMessageFontColor = mxchatChat.bot_message_font_color;
1550 1270 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1551 1271
1552 - // Build thinking dots HTML - skip inline colors if AI theme is active
1553 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1272 +
1554 1273 var thinkingHtml = '<div class="thinking-dots-container">' +
1555 1274 '<div class="thinking-dots">' +
1556 - '<span class="dot"' + dotStyle + '></span>' +
1557 - '<span class="dot"' + dotStyle + '></span>' +
1558 - '<span class="dot"' + dotStyle + '></span>' +
1275 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1276 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1277 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1559 1278 '</div>' +
1560 1279 '</div>';
1561 1280
1562 - // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1563 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1564 - $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1565 - scrollToBottom(botId);
1281 + // Append the thinking dots to the chat container (or within the temporary message div)
1282 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1283 + scrollToBottom();
1566 1284 }
1567 -
1568 - function removeThinkingDots(botId) {
1569 - botId = botId || 'default';
1570 - var $chatBox = getElement(botId, 'chat-box');
1571 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1572 - $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1573 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1285 +
1286 + function removeThinkingDots() {
1287 + $('.thinking-dots').closest('.temporary-message').remove();
1574 1288 }
1575 1289
1576 1290 // ====================================
1577 1291 // TEXT FORMATTING & PROCESSING
@@ -1605,12 +1319,9 @@
1605 1319 processedText = formatTextStyling(processedText);
1606 1320
1607 1321 // Process code blocks BEFORE processing links
1608 1322 processedText = formatCodeBlocks(processedText);
1609 -
1610 - // Process markdown tables BEFORE converting newlines to paragraphs
1611 - processedText = formatMarkdownTables(processedText);
1612 -
1323 +
1613 1324 // NOW convert to paragraphs
1614 1325 processedText = convertNewlinesToBreaks(processedText);
1615 1326
1616 1327 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1623,63 +1334,37 @@
1623 1334 // Return as a proper link without the brackets
1624 1335 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1625 1336 });
1626 1337
1627 - // Process markdown links: [text](url) and [](url)
1628 - // Uses balanced parenthesis matching to handle URLs containing parens
1629 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1630 - processedText = (function(input) {
1631 - var result = '';
1632 - var i = 0;
1633 - while (i < input.length) {
1634 - // Look for [ at current position
1635 - if (input[i] === '[') {
1636 - // Find closing ]
1637 - var closeBracket = input.indexOf(']', i + 1);
1638 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1639 - result += input[i];
1640 - i++;
1641 - continue;
1642 - }
1643 - var linkText = input.substring(i + 1, closeBracket);
1644 - // Check if URL starts with http
1645 - var urlStart = closeBracket + 2;
1646 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1647 - result += input[i];
1648 - i++;
1649 - continue;
1650 - }
1651 - // Find balanced closing paren
1652 - var depth = 1;
1653 - var j = urlStart;
1654 - while (j < input.length && depth > 0) {
1655 - if (input[j] === '(') depth++;
1656 - else if (input[j] === ')') depth--;
1657 - if (depth > 0) j++;
1658 - }
1659 - if (depth !== 0) {
1660 - result += input[i];
1661 - i++;
1662 - continue;
1663 - }
1664 - var url = input.substring(urlStart, j);
1665 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1666 - var encodedUrl = safeEncodeUrl(cleanUrl);
1667 - if (!linkText || !linkText.trim()) {
1668 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1669 - } else {
1670 - var safeText = sanitizeUserInput(linkText);
1671 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1672 - }
1673 - i = j + 1; // Skip past the closing )
1674 - } else {
1675 - result += input[i];
1676 - i++;
1677 - }
1338 + // Process proper markdown links with text: [text](url)
1339 + // This MUST have non-empty text in the first brackets
1340 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1341 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1342 + // Make sure we have actual text (not just whitespace)
1343 + if (!text || !text.trim()) {
1344 + // If no text, treat the URL as the text
1345 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1346 + const safeUrl = safeEncodeUrl(cleanUrl);
1347 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1678 1348 }
1679 - return result;
1680 - })(processedText);
1349 +
1350 + // Clean the URL
1351 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1352 + const safeUrl = safeEncodeUrl(cleanUrl);
1353 + const safeText = sanitizeUserInput(text);
1354 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1355 + });
1681 1356
1357 + // Handle empty markdown links: [](url)
1358 + // This is a specific case where there's no text
1359 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1360 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1361 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1362 + const safeUrl = safeEncodeUrl(cleanUrl);
1363 + // Use the URL itself as the link text
1364 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1365 + });
1366 +
1682 1367 // Process phone numbers: [text](tel:number)
1683 1368 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1684 1369 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1685 1370 const safePhone = safeEncodeUrl(phone);
@@ -1831,78 +1516,9 @@
1831 1516 });
1832 1517
1833 1518 return text;
1834 1519 }
1835 -
1836 - function formatMarkdownTables(text) {
1837 - var lines = text.split('\n');
1838 - var result = [];
1839 - var i = 0;
1840 -
1841 - while (i < lines.length) {
1842 - // Check for a table: current line has pipes AND next line is a separator row
1843 - if (i + 1 < lines.length &&
1844 - lines[i].indexOf('|') !== -1 &&
1845 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1846 -
1847 - var tableLines = [];
1848 - var headerLine = lines[i];
1849 - var separatorLine = lines[i + 1];
1850 - tableLines.push(headerLine);
1851 - tableLines.push(separatorLine);
1852 -
1853 - // Collect remaining table rows
1854 - var j = i + 2;
1855 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1856 - tableLines.push(lines[j]);
1857 - j++;
1858 - }
1859 -
1860 - // Parse alignment from separator row
1861 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1862 - var alignments = sepCells.map(function(cell) {
1863 - var trimmed = cell.trim();
1864 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1865 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1866 - return 'left';
1867 - });
1868 -
1869 - // Build HTML table
1870 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1871 -
1872 - // Header row
1873 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1874 - html += '<thead><tr>';
1875 - headerCells.forEach(function(cell, idx) {
1876 - var align = alignments[idx] || 'left';
1877 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1878 - });
1879 - html += '</tr></thead>';
1880 -
1881 - // Body rows
1882 - html += '<tbody>';
1883 - for (var r = 2; r < tableLines.length; r++) {
1884 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1885 - html += '<tr>';
1886 - rowCells.forEach(function(cell, idx) {
1887 - var align = alignments[idx] || 'left';
1888 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1889 - });
1890 - html += '</tr>';
1891 - }
1892 - html += '</tbody></table></div>';
1893 -
1894 - result.push(html);
1895 - i = j;
1896 - } else {
1897 - result.push(lines[i]);
1898 - i++;
1899 - }
1900 - }
1901 -
1902 - return result.join('\n');
1903 - }
1904 -
1520 +
1905 1521 function sanitizeUserInput(text) {
1906 1522 const div = document.createElement('div');
1907 1523 div.textContent = text;
1908 1524 return div.innerHTML;
@@ -1932,21 +1548,10 @@
1932 1548 // ====================================
1933 1549 // UI & SCROLLING CONTROLS
1934 1550 // ====================================
1935 1551
1936 - function scrollToBottom(botIdOrInstant, instant) {
1937 - // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
1938 - var botId = 'default';
1939 - if (typeof botIdOrInstant === 'string') {
1940 - botId = botIdOrInstant;
1941 - instant = instant || false;
1942 - } else if (typeof botIdOrInstant === 'boolean') {
1943 - instant = botIdOrInstant;
1944 - } else {
1945 - instant = false;
1946 - }
1947 -
1948 - var chatBox = getElement(botId, 'chat-box');
1552 + function scrollToBottom(instant = false) {
1553 + var chatBox = $('#chat-box');
1949 1554 if (instant) {
1950 1555 // Instantly set the scroll position to the bottom
1951 1556 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1952 1557 } else {
@@ -1955,15 +1560,15 @@
1955 1560 const scrollHeight = chatBox.prop("scrollHeight");
1956 1561 const initialScroll = chatBox.scrollTop();
1957 1562 const distance = scrollHeight - initialScroll;
1958 1563 const duration = 500; // Duration in ms
1959 -
1564 +
1960 1565 function smoothScroll(timestamp) {
1961 1566 if (!start) start = timestamp;
1962 1567 const progress = timestamp - start;
1963 1568 const currentScroll = initialScroll + (distance * (progress / duration));
1964 1569 chatBox.scrollTop(currentScroll);
1965 -
1570 +
1966 1571 if (progress < duration) {
1967 1572 requestAnimationFrame(smoothScroll);
1968 1573 } else {
1969 1574 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -1968,36 +1573,31 @@
1968 1573 } else {
1969 1574 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1970 1575 }
1971 1576 }
1972 -
1577 +
1973 1578 requestAnimationFrame(smoothScroll);
1974 1579 }
1975 1580 }
1976 -
1977 - function scrollElementToTop(element, botId) {
1978 - botId = botId || 'default';
1979 - var chatBox = getElement(botId, 'chat-box');
1581 +
1582 + function scrollElementToTop(element) {
1583 + var chatBox = $('#chat-box');
1980 1584 var elementTop = element.position().top + chatBox.scrollTop();
1981 1585 chatBox.animate({ scrollTop: elementTop }, 500);
1982 1586 }
1983 -
1984 - function showChatWidget(botId) {
1985 - botId = botId || 'default';
1986 - var $button = getElement(botId, 'floating-chatbot-button');
1587 +
1588 + function showChatWidget() {
1987 1589 // First ensure display is set
1988 - $button.css('display', 'flex');
1590 + $('#floating-chatbot-button').css('display', 'flex');
1989 1591 // Then handle the fade
1990 - $button.fadeTo(500, 1);
1592 + $('#floating-chatbot-button').fadeTo(500, 1);
1991 1593 // Force visibility
1992 - $button.removeClass('hidden');
1594 + $('#floating-chatbot-button').removeClass('hidden');
1993 1595 }
1994 1596
1995 - function hideChatWidget(botId) {
1996 - botId = botId || 'default';
1997 - var $button = getElement(botId, 'floating-chatbot-button');
1998 - $button.css('display', 'none');
1999 - $button.addClass('hidden');
1597 + function hideChatWidget() {
1598 + $('#floating-chatbot-button').css('display', 'none');
1599 + $('#floating-chatbot-button').addClass('hidden');
2000 1600 }
2001 1601
2002 1602 function disableScroll() {
2003 1603 if (isMobile()) {
@@ -2056,43 +1656,34 @@
2056 1656 chatButton.appendChild(notificationBadge);
2057 1657
2058 1658 }
2059 1659
2060 - function showNotification(botId) {
2061 - botId = botId || 'default';
2062 - const badge = getElementDOM(botId, 'chat-notification-badge');
2063 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2064 - if (badge && $floatingChatbot.hasClass('hidden')) {
1660 + function showNotification() {
1661 + const badge = document.getElementById('chat-notification-badge');
1662 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2065 1663 badge.style.display = 'block';
2066 1664 badge.textContent = '1';
2067 1665 }
2068 1666 }
2069 -
2070 - function hideNotification(botId) {
2071 - botId = botId || 'default';
2072 - const badge = getElementDOM(botId, 'chat-notification-badge');
1667 +
1668 + function hideNotification() {
1669 + const badge = document.getElementById('chat-notification-badge');
2073 1670 if (badge) {
2074 1671 badge.style.display = 'none';
2075 1672 }
2076 1673 }
2077 -
2078 - function startNotificationChecking(botId) {
2079 - botId = botId || 'default';
1674 +
1675 + function startNotificationChecking() {
2080 1676 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2081 1677 if (!chatPersistenceEnabled) return;
2082 -
2083 - createNotificationBadge(botId);
2084 - var instance = MxChatInstances.get(botId);
2085 - instance.notificationCheckInterval = setInterval(function() {
2086 - checkForNewMessages(botId);
2087 - }, 30000); // Check every 30 seconds
1678 +
1679 + createNotificationBadge();
1680 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2088 1681 }
2089 -
2090 - function stopNotificationChecking(botId) {
2091 - botId = botId || 'default';
2092 - var instance = MxChatInstances.get(botId);
2093 - if (instance.notificationCheckInterval) {
2094 - clearInterval(instance.notificationCheckInterval);
1682 +
1683 + function stopNotificationChecking() {
1684 + if (notificationCheckInterval) {
1685 + clearInterval(notificationCheckInterval);
2095 1686 }
2096 1687 }
2097 1688
2098 1689 function checkForNewMessages() {
@@ -2118,35 +1709,28 @@
2118 1709 });
2119 1710 }
2120 1711
2121 1712
2122 -// ====================================
2123 -// LIVE AGENT FUNCTIONALITY
2124 -// ====================================
1713 + // ====================================
1714 + // LIVE AGENT FUNCTIONALITY
1715 + // ====================================
2125 1716
2126 -function startPolling(botId) {
2127 - botId = botId || 'default';
2128 - var instance = MxChatInstances.get(botId);
2129 - // Clear any existing interval first
2130 - stopPolling(botId);
2131 - instance.pollingInterval = setInterval(function() {
2132 - checkForAgentMessages(botId);
2133 - }, 5000);
2134 -}
1717 + function startPolling() {
1718 + // Clear any existing interval first
1719 + stopPolling();
1720 + // Start new polling interval
1721 + pollingInterval = setInterval(checkForAgentMessages, 5000);
1722 + }
2135 1723
2136 -function stopPolling(botId) {
2137 - botId = botId || 'default';
2138 - var instance = MxChatInstances.get(botId);
2139 - if (instance.pollingInterval) {
2140 - clearInterval(instance.pollingInterval);
2141 - instance.pollingInterval = null;
1724 + function stopPolling() {
1725 + if (pollingInterval) {
1726 + clearInterval(pollingInterval);
1727 + pollingInterval = null;
1728 + }
2142 1729 }
2143 -}
2144 -
2145 -function checkForAgentMessages(botId) {
2146 - botId = botId || 'default';
2147 - var instance = MxChatInstances.get(botId);
2148 - const sessionId = getChatSession(botId);
1730 +
1731 +function checkForAgentMessages() {
1732 + const sessionId = getChatSession();
2149 1733 $.ajax({
2150 1734 url: mxchatChat.ajax_url,
2151 1735 type: 'POST',
2152 1736 dataType: 'json',
@@ -2152,41 +1736,32 @@
2152 1736 dataType: 'json',
2153 1737 data: {
2154 1738 action: 'mxchat_fetch_new_messages',
2155 1739 session_id: sessionId,
2156 - last_seen_id: instance.lastSeenMessageId,
2157 - persistence_enabled: 'true',
1740 + last_seen_id: lastSeenMessageId,
1741 + persistence_enabled: 'true', // Add this too
2158 1742 nonce: mxchatChat.nonce
2159 1743 },
2160 1744 success: function (response) {
2161 1745 if (response.success && response.data?.new_messages) {
2162 1746 let hasNewMessage = false;
2163 -
1747 +
2164 1748 response.data.new_messages.forEach(function (message) {
2165 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1749 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2166 1750 hasNewMessage = true;
2167 - appendMessage("agent", message.content, '', [], false, botId);
2168 - instance.lastSeenMessageId = message.id;
2169 - instance.processedMessageIds.add(message.id);
1751 + // CHANGE THIS LINE:
1752 + appendMessage("agent", message.content); // Instead of replaceLastMessage
1753 + lastSeenMessageId = message.id;
1754 + processedMessageIds.add(message.id);
2170 1755 }
2171 1756 });
2172 1757
2173 - if (hasNewMessage) {
2174 - enableChatInput(botId);
1758 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1759 + showNotification();
2175 1760 }
2176 -
2177 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2178 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2179 - showNotification(botId);
2180 - }
2181 -
2182 - scrollToBottom(botId, true);
1761 +
1762 + scrollToBottom(true);
2183 1763 }
2184 -
2185 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2186 - if (response.success && response.data?.chat_mode) {
2187 - updateChatModeIndicator(response.data.chat_mode, botId);
2188 - }
2189 1764 },
2190 1765 error: function (xhr, status, error) {
2191 1766 // Polling error - silently continue
2192 1767 }
@@ -2196,29 +1771,17 @@
2196 1771 // ====================================
2197 1772 // CHAT HISTORY & PERSISTENCE
2198 1773 // ====================================
2199 1774
2200 -function loadChatHistory(botId, onComplete) {
2201 - botId = botId || 'default';
2202 - var instance = MxChatInstances.get(botId);
2203 -
1775 +function loadChatHistory() {
2204 1776 // Prevent duplicate loading
2205 - if (instance.chatHistoryLoaded) {
2206 - if (onComplete) onComplete();
1777 + if (chatHistoryLoaded) {
2207 1778 return;
2208 1779 }
2209 -
2210 - // Use getChatSession which returns null if no session exists (does NOT create one)
2211 - var sessionId = getChatSession(botId);
1780 +
1781 + var sessionId = getChatSession();
2212 1782 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2213 1783
2214 - // No session yet — nothing to load. History will load after first message via ensureSession.
2215 - if (!sessionId) {
2216 - instance.chatHistoryLoaded = true;
2217 - if (onComplete) onComplete();
2218 - return;
2219 - }
2220 -
2221 1784 if (chatPersistenceEnabled && sessionId) {
2222 1785 $.ajax({
2223 1786 url: mxchatChat.ajax_url,
2224 1787 type: 'POST',
@@ -2229,12 +1792,11 @@
2229 1792 },
2230 1793 success: function(response) {
2231 1794 // Handle session reset (IP changed while user was away)
2232 1795 if (response.success === false && response.data && response.data.action === 'reset_session') {
2233 - // Silent reset — new session but don't clear UI
2234 - MxChatInstances.silentResetSession(botId);
2235 - instance.chatHistoryLoaded = true; // Prevent retry loop
2236 - if (onComplete) onComplete();
1796 + // Silently reset session - user will start fresh
1797 + resetChatSession();
1798 + chatHistoryLoaded = true; // Prevent retry loop
2237 1799 return;
2238 1800 }
2239 1801
2240 1802 // Check if the response indicates success
@@ -2240,15 +1802,15 @@
2240 1802 // Check if the response indicates success
2241 1803 if (response.success) {
2242 1804 // Handle case where conversation data exists and is an array
2243 1805 if (response.data && Array.isArray(response.data.conversation)) {
2244 - var $chatBox = getElement(botId, 'chat-box');
1806 + var $chatBox = $('#chat-box');
2245 1807 var $fragment = $(document.createDocumentFragment());
2246 - let highestMessageId = instance.lastSeenMessageId;
1808 + let highestMessageId = lastSeenMessageId;
2247 1809
2248 1810 // Update chat mode if provided
2249 1811 if (response.data.chat_mode) {
2250 - updateChatModeIndicator(response.data.chat_mode, botId);
1812 + updateChatModeIndicator(response.data.chat_mode);
2251 1813 }
2252 1814
2253 1815 // Only process if there are actual messages
2254 1816 if (response.data.conversation.length > 0) {
@@ -2253,9 +1815,9 @@
2253 1815 // Only process if there are actual messages
2254 1816 if (response.data.conversation.length > 0) {
2255 1817 // IMPORTANT: Clear existing messages before loading history
2256 1818 $chatBox.empty();
2257 -
1819 +
2258 1820 $.each(response.data.conversation, function(index, message) {
2259 1821 // Skip agent messages if persistence is off
2260 1822 if (!chatPersistenceEnabled && message.role === 'agent') {
2261 1823 return;
@@ -2290,19 +1852,9 @@
2290 1852 var content = message.content;
2291 1853 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2292 1854 content = decodeHTMLEntities(content);
2293 1855
2294 - // Skip linkify for messages containing structured HTML
2295 - // (forms, product cards, galleries, etc.) to avoid
2296 - // markdown formatting corrupting HTML attributes
2297 - // (e.g. underscores in name="field_name" becoming <em> tags)
2298 - if (content.includes("mxchat-product-card") ||
2299 - content.includes("mxchat-image-gallery") ||
2300 - content.includes("mxchat-featured-products") ||
2301 - content.includes("<form") ||
2302 - content.includes("<input") ||
2303 - content.includes("<select") ||
2304 - content.includes("<textarea")) {
1856 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2305 1857 messageElement.html(content);
2306 1858 } else {
2307 1859 var formattedContent = linkify(content);
2308 1860 messageElement.html(formattedContent);
@@ -2312,47 +1864,42 @@
2312 1864
2313 1865 // Track message IDs
2314 1866 if (message.id) {
2315 1867 highestMessageId = Math.max(highestMessageId, message.id);
2316 - instance.processedMessageIds.add(message.id);
1868 + processedMessageIds.add(message.id);
2317 1869 }
2318 1870 });
2319 1871
2320 1872 // Only append messages and scroll if we have content
2321 1873 $chatBox.append($fragment);
2322 - scrollToBottom(botId, true);
1874 + scrollToBottom(true);
2323 1875
2324 1876 // Collapse quick questions if we have conversation history
2325 - // BUT skip auto-collapse for embedded bots (they should stay expanded)
2326 - if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2327 - collapseQuickQuestions(botId);
1877 + if (hasQuickQuestions()) {
1878 + collapseQuickQuestions();
2328 1879 }
2329 1880
2330 1881 // Update lastSeenMessageId after history loads
2331 - instance.lastSeenMessageId = highestMessageId;
1882 + lastSeenMessageId = highestMessageId;
2332 1883
2333 1884 // Only update chat mode if persistence is enabled and we have messages
2334 1885 if (chatPersistenceEnabled) {
2335 1886 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2336 1887 if (lastMessage.role === 'agent') {
2337 - updateChatModeIndicator('agent', botId);
1888 + updateChatModeIndicator('agent');
2338 1889 }
2339 1890 }
2340 -
1891 +
2341 1892 // Mark as loaded ONLY after successful load
2342 - instance.chatHistoryLoaded = true;
1893 + chatHistoryLoaded = true;
2343 1894 }
2344 1895 }
2345 1896 }
2346 - if (onComplete) onComplete();
2347 1897 },
2348 1898 error: function(xhr, status, error) {
2349 1899 // Error loading chat history - silently continue
2350 - if (onComplete) onComplete();
2351 1900 }
2352 1901 });
2353 - } else {
2354 - if (onComplete) onComplete();
2355 1902 }
2356 1903 }
2357 1904
2358 1905
@@ -2366,12 +1913,11 @@
2366 1913 element.addEventListener(eventType, handler);
2367 1914 }
2368 1915 }
2369 1916
2370 - function showActivePdf(filename, botId) {
2371 - botId = botId || 'default';
2372 - const container = getElementDOM(botId, 'active-pdf-container');
2373 - const nameElement = getElementDOM(botId, 'active-pdf-name');
1917 + function showActivePdf(filename) {
1918 + const container = document.getElementById('active-pdf-container');
1919 + const nameElement = document.getElementById('active-pdf-name');
2374 1920
2375 1921 if (!container || !nameElement) {
2376 1922 return;
2377 1923 }
@@ -2379,12 +1925,11 @@
2379 1925 nameElement.textContent = filename;
2380 1926 container.style.display = 'flex';
2381 1927 }
2382 1928
2383 - function showActiveWord(filename, botId) {
2384 - botId = botId || 'default';
2385 - const container = getElementDOM(botId, 'active-word-container');
2386 - const nameElement = getElementDOM(botId, 'active-word-name');
1929 + function showActiveWord(filename) {
1930 + const container = document.getElementById('active-word-container');
1931 + const nameElement = document.getElementById('active-word-name');
2387 1932
2388 1933 if (!container || !nameElement) {
2389 1934 return;
2390 1935 }
@@ -2391,17 +1936,15 @@
2391 1936
2392 1937 nameElement.textContent = filename;
2393 1938 container.style.display = 'flex';
2394 1939 }
2395 -
2396 - function removeActivePdf(botId) {
2397 - botId = botId || 'default';
2398 - var instance = MxChatInstances.get(botId);
2399 - const container = getElementDOM(botId, 'active-pdf-container');
2400 - const nameElement = getElementDOM(botId, 'active-pdf-name');
2401 -
2402 - if (!container || !nameElement || !instance.activePdfFile) return;
2403 -
1940 +
1941 + function removeActivePdf() {
1942 + const container = document.getElementById('active-pdf-container');
1943 + const nameElement = document.getElementById('active-pdf-name');
1944 +
1945 + if (!container || !nameElement || !activePdfFile) return;
1946 +
2404 1947 fetch(mxchatChat.ajax_url, {
2405 1948 method: 'POST',
2406 1949 headers: {
2407 1950 'Content-Type': 'application/x-www-form-urlencoded',
@@ -2407,9 +1950,9 @@
2407 1950 'Content-Type': 'application/x-www-form-urlencoded',
2408 1951 },
2409 1952 body: new URLSearchParams({
2410 1953 'action': 'mxchat_remove_pdf',
2411 - 'session_id': getChatSession(botId),
1954 + 'session_id': sessionId,
2412 1955 'nonce': mxchatChat.nonce
2413 1956 })
2414 1957 })
2415 1958 .then(response => response.json())
@@ -2460,10 +2003,9 @@
2460 2003 // ====================================
2461 2004 // CONSENT & COMPLIANCE (GDPR)
2462 2005 // ====================================
2463 2006
2464 - function initializeChatVisibility(botId) {
2465 - botId = botId || 'default';
2007 + function initializeChatVisibility() {
2466 2008 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2467 2009 mxchatChat.complianz_toggle === '1' ||
2468 2010 mxchatChat.complianz_toggle === 1;
2469 2011
@@ -2468,35 +2010,34 @@
2468 2010 mxchatChat.complianz_toggle === 1;
2469 2011
2470 2012 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2471 2013 // Initial check
2472 - checkConsentAndShowChat(botId);
2014 + checkConsentAndShowChat();
2473 2015
2474 2016 // Listen for consent changes
2475 2017 $(document).on('cmplz_status_change', function(event) {
2476 - checkConsentAndShowChat(botId);
2018 + checkConsentAndShowChat();
2477 2019 });
2478 2020 } else {
2479 2021 // If Complianz is not enabled, always show
2480 - getElement(botId, 'floating-chatbot-button')
2022 + $('#floating-chatbot-button')
2481 2023 .css('display', 'flex')
2482 2024 .removeClass('hidden no-consent')
2483 2025 .fadeTo(500, 1);
2484 -
2026 +
2485 2027 // Also check pre-chat message when Complianz is not enabled
2486 - checkPreChatDismissal(botId);
2028 + checkPreChatDismissal();
2487 2029 }
2488 2030 }
2489 2031
2490 -
2491 - function checkConsentAndShowChat(botId) {
2492 - botId = botId || 'default';
2032 +
2033 + function checkConsentAndShowChat() {
2493 2034 var consentStatus = cmplz_has_consent('marketing');
2494 2035 var consentType = complianz.consenttype;
2495 2036
2496 - let $widget = getElement(botId, 'floating-chatbot-button');
2497 - let $chatbot = getElement(botId, 'floating-chatbot');
2498 - let $preChat = getElement(botId, 'pre-chat-message');
2037 + let $widget = $('#floating-chatbot-button');
2038 + let $chatbot = $('#floating-chatbot');
2039 + let $preChat = $('#pre-chat-message');
2499 2040
2500 2041 if (consentStatus === true) {
2501 2042 $widget
2502 2043 .removeClass('no-consent')
@@ -2505,9 +2046,9 @@
2505 2046 .fadeTo(500, 1);
2506 2047 $chatbot.removeClass('no-consent');
2507 2048
2508 2049 // Show pre-chat message if not dismissed
2509 - checkPreChatDismissal(botId);
2050 + checkPreChatDismissal();
2510 2051 } else {
2511 2052 $widget
2512 2053 .addClass('no-consent')
2513 2054 .fadeTo(500, 0, function() {
@@ -2525,38 +2066,46 @@
2525 2066
2526 2067 // ====================================
2527 2068 // PRE-CHAT MESSAGE HANDLING
2528 2069 // ====================================
2529 -
2530 - function checkPreChatDismissal(botId) {
2531 - botId = botId || 'default';
2532 - try {
2533 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2534 - if (dismissedAt) {
2535 - // Re-show after 24 hours
2536 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2537 - if (elapsed < 86400000) {
2538 - getElement(botId, 'pre-chat-message').hide();
2539 - return;
2070 +
2071 + function checkPreChatDismissal() {
2072 + $.ajax({
2073 + url: mxchatChat.ajax_url,
2074 + type: 'POST',
2075 + data: {
2076 + action: 'mxchat_check_pre_chat_message_status',
2077 + _ajax_nonce: mxchatChat.nonce
2078 + },
2079 + success: function(response) {
2080 + if (response.success && !response.data.dismissed) {
2081 + $('#pre-chat-message').fadeIn(250);
2082 + } else {
2083 + $('#pre-chat-message').hide();
2540 2084 }
2541 - // Expired — clear and show again
2542 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2085 + },
2086 + error: function() {
2087 + // Error checking pre-chat dismissal - silently continue
2543 2088 }
2544 - getElement(botId, 'pre-chat-message').fadeIn(250);
2545 - } catch (e) {
2546 - // localStorage unavailable — show the message
2547 - getElement(botId, 'pre-chat-message').fadeIn(250);
2548 - }
2089 + });
2549 2090 }
2550 2091
2551 - function handlePreChatDismissal(botId) {
2552 - botId = botId || 'default';
2553 - getElement(botId, 'pre-chat-message').fadeOut(200);
2554 - try {
2555 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2556 - } catch (e) {
2557 - // localStorage unavailable — dismissal won't persist
2558 - }
2092 + function handlePreChatDismissal() {
2093 + $('#pre-chat-message').fadeOut(200);
2094 + $.ajax({
2095 + url: mxchatChat.ajax_url,
2096 + type: 'POST',
2097 + data: {
2098 + action: 'mxchat_dismiss_pre_chat_message',
2099 + _ajax_nonce: mxchatChat.nonce
2100 + },
2101 + success: function() {
2102 + $('#pre-chat-message').hide();
2103 + },
2104 + error: function() {
2105 + // Error dismissing pre-chat message - silently continue
2106 + }
2107 + });
2559 2108 }
2560 2109
2561 2110
2562 2111 // ====================================
@@ -2582,104 +2131,78 @@
2582 2131 // ====================================
2583 2132
2584 2133 $(document).on('click', '.mxchat-popular-question', function () {
2585 2134 var question = $(this).text();
2586 - var botId = getBotIdFromElement(this);
2587 -
2135 +
2588 2136 // Append the question as if the user typed it
2589 - appendMessage("user", question, '', [], false, botId);
2590 -
2137 + appendMessage("user", question);
2138 +
2591 2139 // Only collapse if there are questions
2592 - if (hasQuickQuestions(botId)) {
2593 - collapseQuickQuestions(botId);
2140 + if (hasQuickQuestions()) {
2141 + collapseQuickQuestions();
2594 2142 }
2595 -
2143 +
2596 2144 // Send the question to the server
2597 - sendMessageToChatbot(question, botId);
2145 + sendMessageToChatbot(question);
2598 2146 });
2599 2147
2600 2148 $(document).on('click', '.questions-toggle-btn', function(e) {
2601 2149 e.preventDefault();
2602 2150 e.stopPropagation();
2603 - var botId = getBotIdFromElement(this);
2604 - expandQuickQuestions(botId);
2151 + expandQuickQuestions();
2605 2152 });
2606 2153
2607 2154 $(document).on('click', '.questions-collapse-btn', function(e) {
2608 2155 e.preventDefault();
2609 2156 e.stopPropagation();
2610 - var botId = getBotIdFromElement(this);
2611 - collapseQuickQuestions(botId);
2157 + collapseQuickQuestions();
2612 2158 });
2613 2159
2614 - // Chatbot visibility toggle handlers - use class selector for multi-instance support
2615 - $(document).on('click', '.floating-chatbot-button', function() {
2616 - var botId = getBotIdFromElement(this);
2617 - var $chatbot = getElement(botId, 'floating-chatbot');
2618 - var $badge = getElement(botId, 'chat-notification-badge');
2619 - var $preChat = getElement(botId, 'pre-chat-message');
2620 -
2621 - if ($chatbot.hasClass('hidden')) {
2622 - $chatbot.removeClass('hidden').addClass('visible');
2160 + // Chatbot visibility toggle handlers
2161 + $(document).on('click', '#floating-chatbot-button', function() {
2162 + var chatbot = $('#floating-chatbot');
2163 + if (chatbot.hasClass('hidden')) {
2164 + chatbot.removeClass('hidden').addClass('visible');
2623 2165 $(this).addClass('hidden');
2624 - $badge.hide(); // Hide notification when opening chat
2166 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
2625 2167 disableScroll();
2626 - $preChat.fadeOut(250);
2627 -
2628 - // Load chat history for returning visitors (persistence)
2629 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2630 - if (chatPersistenceEnabled) {
2631 - MxChatInstances.ensureSession(botId);
2632 - }
2633 -
2634 - // Deferred email check — only on first widget open
2635 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2636 - var instance = MxChatInstances.get(botId);
2637 - if (emailBlocker && !instance.emailCheckDone) {
2638 - instance.emailCheckDone = true;
2639 - resolveEmailState(botId);
2640 - } else if (!emailBlocker) {
2641 - // No email collection — still route through showChatContainerForBot
2642 - // so the loader is shown while chat history loads
2643 - showChatContainerForBot(botId);
2644 - }
2168 + $('#pre-chat-message').fadeOut(250);
2645 2169 } else {
2646 - $chatbot.removeClass('visible').addClass('hidden');
2170 + chatbot.removeClass('visible').addClass('hidden');
2647 2171 $(this).removeClass('hidden');
2648 2172 enableScroll();
2649 - checkPreChatDismissal(botId);
2173 + checkPreChatDismissal();
2650 2174 }
2651 2175 });
2652 -
2653 - // Allow clicking anywhere on the title bar to close the chatbot
2654 - $(document).on('click', '.chatbot-top-bar', function() {
2655 - var botId = getBotIdFromElement(this);
2656 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2657 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
2176 +
2177 + $(document).on('click', '#exit-chat-button', function() {
2178 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
2179 + $('#floating-chatbot-button').removeClass('hidden');
2658 2180 enableScroll();
2659 2181 });
2660 -
2182 +
2661 2183 $(document).on('click', '.close-pre-chat-message', function(e) {
2662 2184 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2663 - var botId = getBotIdFromElement(this);
2664 - handlePreChatDismissal(botId);
2185 + $('#pre-chat-message').fadeOut(200, function() {
2186 + $(this).remove();
2187 + });
2665 2188 });
2189 +
2666 2190
2667 -
2668 - // PDF upload button handlers - use class selector
2669 - $(document).on('click', '.pdf-upload-btn', function() {
2670 - var botId = getBotIdFromElement(this);
2671 - var pdfInput = getElementDOM(botId, 'pdf-upload');
2672 - if (pdfInput) pdfInput.click();
2673 - });
2674 -
2675 - // Word upload button handlers - use class selector
2676 - $(document).on('click', '.word-upload-btn', function() {
2677 - var botId = getBotIdFromElement(this);
2678 - var wordInput = getElementDOM(botId, 'word-upload');
2679 - if (wordInput) wordInput.click();
2680 - });
2191 + // PDF upload button handlers
2192 + if (document.getElementById('pdf-upload-btn')) {
2193 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2194 + document.getElementById('pdf-upload').click();
2195 + });
2196 + }
2681 2197
2198 + // Word upload button handlers
2199 + if (document.getElementById('word-upload-btn')) {
2200 + document.getElementById('word-upload-btn').addEventListener('click', function() {
2201 + document.getElementById('word-upload').click();
2202 + });
2203 + }
2204 +
2682 2205 // PDF file input change handler
2683 2206 addSafeEventListener('pdf-upload', 'change', async function(e) {
2684 2207 const file = e.target.files[0];
2685 2208
@@ -2863,497 +2386,431 @@
2863 2386 });
2864 2387
2865 2388
2866 2389 // ====================================
2867 -// INIT LOADER & CHAT CONTAINER HELPERS
2390 +// EMAIL COLLECTION SETUP - FIXED VERSION
2868 2391 // ====================================
2869 -// These must be outside the email collection block so they're always available
2870 -// (used by persistence loading even when email collection is off)
2871 -
2872 -function showInitLoader(botId) {
2873 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2874 - if (loader) loader.style.display = 'flex';
2875 -}
2876 -
2877 -function hideInitLoader(botId) {
2878 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2879 - if (loader) loader.style.display = 'none';
2880 -}
2881 -
2882 -function showEmailFormForBot(botId) {
2883 - hideInitLoader(botId);
2884 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2885 - var chatContainer = getElementDOM(botId, 'chat-container');
2886 - if (emailBlocker) emailBlocker.style.display = 'flex';
2887 - if (chatContainer) chatContainer.style.display = 'none';
2888 -}
2889 -
2890 -function showChatContainerForBot(botId) {
2891 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2892 - var chatContainer = getElementDOM(botId, 'chat-container');
2893 - if (emailBlocker) emailBlocker.style.display = 'none';
2894 -
2895 - var instance = MxChatInstances.get(botId);
2896 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2897 -
2898 - // If persistence is on and history hasn't loaded yet, show loader
2899 - // while history loads to prevent flash of empty chat
2900 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2901 - if (chatContainer) chatContainer.style.display = 'none';
2902 - showInitLoader(botId);
2903 - loadChatHistory(botId, function() {
2904 - hideInitLoader(botId);
2905 - if (chatContainer) chatContainer.style.display = 'flex';
2906 - scrollToBottom(botId, true);
2907 - });
2908 - } else {
2909 - hideInitLoader(botId);
2910 - if (chatContainer) chatContainer.style.display = 'flex';
2911 - if (typeof loadChatHistory === 'function') {
2912 - loadChatHistory(botId);
2913 - }
2914 - }
2915 -}
2916 -
2917 -// ====================================
2918 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2919 -// ====================================
2920 2392 // Only run email collection setup if it's enabled
2921 2393 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2394 + // Email collection form setup and handlers
2395 + const emailForm = document.getElementById('email-collection-form');
2396 + const emailBlocker = document.getElementById('email-blocker');
2397 + const chatbotWrapper = document.getElementById('chat-container');
2922 2398
2923 - // Track submitting state per bot
2924 - const emailSubmittingState = {};
2399 + if (emailForm && emailBlocker && chatbotWrapper) {
2400 +
2401 + // Add loading state management
2402 + let isSubmitting = false;
2403 +
2404 + // Optimized UI transition functions
2405 + function showEmailForm() {
2406 + emailBlocker.style.display = 'flex';
2407 + chatbotWrapper.style.display = 'none';
2408 + }
2925 2409
2926 - // Add CSS animations for email form (once globally)
2927 - if (!document.getElementById('email-error-styles')) {
2928 - const style = document.createElement('style');
2929 - style.id = 'email-error-styles';
2930 - style.textContent = `
2931 - @keyframes fadeInError {
2932 - from { opacity: 0; transform: translateY(-5px); }
2933 - to { opacity: 1; transform: translateY(0); }
2410 + function showChatContainer() {
2411 + // Show chat immediately without delay
2412 + emailBlocker.style.display = 'none';
2413 + chatbotWrapper.style.display = 'flex';
2414 +
2415 + // Load chat history only after showing chat container
2416 + if (typeof loadChatHistory === 'function') {
2417 + loadChatHistory();
2934 2418 }
2935 - .email-input-shake {
2936 - animation: shake 0.5s ease-in-out;
2937 - }
2938 - @keyframes shake {
2939 - 0%, 100% { transform: translateX(0); }
2940 - 25% { transform: translateX(-5px); }
2941 - 75% { transform: translateX(5px); }
2942 - }
2943 - @keyframes spin {
2944 - from { transform: rotate(0deg); }
2945 - to { transform: rotate(360deg); }
2946 - }
2947 - .email-spinner {
2948 - display: inline-block;
2949 - vertical-align: middle;
2950 - }
2951 - `;
2952 - document.head.appendChild(style);
2953 - }
2419 + }
2954 2420
2955 - function isValidEmailAddress(email) {
2956 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2957 - return emailRegex.test(email.trim()) && email.length <= 254;
2958 - }
2421 + // Enhanced email validation
2422 + function isValidEmail(email) {
2423 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2424 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2425 + }
2959 2426
2960 - function isValidNameInput(name) {
2961 - return name && name.trim().length >= 2 && name.trim().length <= 100;
2962 - }
2427 + // Enhanced name validation
2428 + function isValidName(name) {
2429 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2430 + }
2963 2431
2964 - /**
2965 - * Replace {visitor_name} placeholder in intro message with actual visitor name
2966 - * @param {string} botId - The bot instance ID
2967 - * @param {string} visitorName - The visitor's name to insert
2968 - */
2969 - function replaceVisitorNamePlaceholder(botId, visitorName) {
2970 - var chatBox = getElementDOM(botId, 'chat-box');
2971 - if (!chatBox) return;
2972 -
2973 - // Find the first bot message (intro message)
2974 - var introMessage = chatBox.querySelector('.bot-message');
2975 - if (!introMessage) return;
2976 -
2977 - var messageContent = introMessage.querySelector('div[dir="auto"]');
2978 - if (!messageContent) return;
2979 -
2980 - var html = messageContent.innerHTML;
2981 -
2982 - // Replace {visitor_name} placeholder (case-insensitive)
2983 - if (visitorName && visitorName.trim()) {
2984 - // Escape HTML to prevent XSS
2985 - var safeName = $('<div>').text(visitorName.trim()).html();
2986 - html = html.replace(/\{visitor_name\}/gi, safeName);
2987 - } else {
2988 - // Remove placeholder and clean up spacing if no name provided
2989 - html = html.replace(/\{visitor_name\}/gi, '');
2990 - // Clean up any double spaces that might result
2991 - html = html.replace(/\s{2,}/g, ' ').trim();
2432 + // Show loading state with spinner
2433 + function setSubmissionState(loading) {
2434 + const submitButton = document.getElementById('email-submit-button');
2435 + const emailInput = document.getElementById('user-email');
2436 + const nameInput = document.getElementById('user-name');
2437 +
2438 + if (loading) {
2439 + isSubmitting = true;
2440 + if (submitButton) submitButton.disabled = true;
2441 + if (emailInput) emailInput.disabled = true;
2442 + if (nameInput) nameInput.disabled = true;
2443 +
2444 + // Store original content and add spinner
2445 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2446 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2447 +
2448 + // Add loading spinner while keeping original text
2449 + const originalText = submitButton.textContent;
2450 + submitButton.innerHTML = `
2451 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2452 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2453 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2454 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2455 + </circle>
2456 + </svg>
2457 + ${originalText}
2458 + `;
2459 +
2460 + submitButton.style.opacity = '0.8';
2461 + }
2462 + } else {
2463 + isSubmitting = false;
2464 + if (submitButton) submitButton.disabled = false;
2465 + if (emailInput) emailInput.disabled = false;
2466 + if (nameInput) nameInput.disabled = false;
2467 +
2468 + // Restore original content
2469 + if (submitButton) {
2470 + const originalHtml = submitButton.getAttribute('data-original-html');
2471 + if (originalHtml) {
2472 + submitButton.innerHTML = originalHtml;
2473 + }
2474 + submitButton.style.opacity = '1';
2475 + }
2476 + }
2992 2477 }
2993 2478
2994 - messageContent.innerHTML = html;
2995 - }
2996 -
2997 - function setEmailSubmissionState(botId, loading) {
2998 - var submitButton = getElementDOM(botId, 'email-submit-button');
2999 - var emailInput = getElementDOM(botId, 'user-email');
3000 - var nameInput = getElementDOM(botId, 'user-name');
3001 -
3002 - if (loading) {
3003 - emailSubmittingState[botId] = true;
3004 - if (submitButton) submitButton.disabled = true;
3005 - if (emailInput) emailInput.disabled = true;
3006 - if (nameInput) nameInput.disabled = true;
3007 -
3008 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3009 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3010 - const originalText = submitButton.textContent;
3011 - submitButton.innerHTML = `
3012 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3013 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3014 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3015 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3016 - </circle>
3017 - </svg>
3018 - ${originalText}
2479 + // Error display functions
2480 + function showEmailError(message) {
2481 + clearEmailError();
2482 +
2483 + const errorDiv = document.createElement('div');
2484 + errorDiv.className = 'email-error';
2485 + errorDiv.style.cssText = `
2486 + color: #e74c3c;
2487 + font-size: 12px;
2488 + margin-top: 8px;
2489 + padding: 4px 0;
2490 + animation: fadeInError 0.3s ease;
2491 + `;
2492 + errorDiv.textContent = message;
2493 +
2494 + // Add CSS animation if not already present
2495 + if (!document.getElementById('email-error-styles')) {
2496 + const style = document.createElement('style');
2497 + style.id = 'email-error-styles';
2498 + style.textContent = `
2499 + @keyframes fadeInError {
2500 + from { opacity: 0; transform: translateY(-5px); }
2501 + to { opacity: 1; transform: translateY(0); }
2502 + }
2503 + .email-input-shake {
2504 + animation: shake 0.5s ease-in-out;
2505 + }
2506 + @keyframes shake {
2507 + 0%, 100% { transform: translateX(0); }
2508 + 25% { transform: translateX(-5px); }
2509 + 75% { transform: translateX(5px); }
2510 + }
2511 + @keyframes spin {
2512 + from { transform: rotate(0deg); }
2513 + to { transform: rotate(360deg); }
2514 + }
2515 + .email-spinner {
2516 + display: inline-block;
2517 + vertical-align: middle;
2518 + }
3019 2519 `;
3020 - submitButton.style.opacity = '0.8';
2520 + document.head.appendChild(style);
3021 2521 }
3022 - } else {
3023 - emailSubmittingState[botId] = false;
3024 - if (submitButton) submitButton.disabled = false;
3025 - if (emailInput) emailInput.disabled = false;
3026 - if (nameInput) nameInput.disabled = false;
3027 -
3028 - if (submitButton) {
3029 - const originalHtml = submitButton.getAttribute('data-original-html');
3030 - if (originalHtml) {
3031 - submitButton.innerHTML = originalHtml;
3032 - }
3033 - submitButton.style.opacity = '1';
2522 +
2523 + emailForm.appendChild(errorDiv);
2524 +
2525 + // Add shake animation to inputs
2526 + const emailInput = document.getElementById('user-email');
2527 + const nameInput = document.getElementById('user-name');
2528 +
2529 + if (emailInput) {
2530 + emailInput.classList.add('email-input-shake');
2531 + setTimeout(() => {
2532 + emailInput.classList.remove('email-input-shake');
2533 + }, 500);
3034 2534 }
2535 +
2536 + if (nameInput) {
2537 + nameInput.classList.add('email-input-shake');
2538 + setTimeout(() => {
2539 + nameInput.classList.remove('email-input-shake');
2540 + }, 500);
2541 + }
3035 2542 }
3036 - }
3037 2543
3038 - function showEmailError(botId, message) {
3039 - clearEmailError(botId);
3040 -
3041 - var emailForm = getElementDOM(botId, 'email-collection-form');
3042 - if (!emailForm) return;
3043 -
3044 - const errorDiv = document.createElement('div');
3045 - errorDiv.className = 'email-error';
3046 - errorDiv.style.cssText = `
3047 - color: #e74c3c;
3048 - font-size: 12px;
3049 - margin-top: 8px;
3050 - padding: 4px 0;
3051 - animation: fadeInError 0.3s ease;
3052 - `;
3053 - errorDiv.textContent = message;
3054 - emailForm.appendChild(errorDiv);
3055 -
3056 - // Add shake animation to inputs
3057 - var emailInput = getElementDOM(botId, 'user-email');
3058 - var nameInput = getElementDOM(botId, 'user-name');
3059 -
3060 - if (emailInput) {
3061 - emailInput.classList.add('email-input-shake');
3062 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3063 - }
3064 - if (nameInput) {
3065 - nameInput.classList.add('email-input-shake');
3066 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3067 - }
3068 - }
3069 -
3070 - function clearEmailError(botId) {
3071 - var emailForm = getElementDOM(botId, 'email-collection-form');
3072 - if (emailForm) {
2544 + function clearEmailError() {
3073 2545 const existingErrors = emailForm.querySelectorAll('.email-error');
3074 2546 existingErrors.forEach(error => error.remove());
3075 2547 }
3076 - }
3077 2548
3078 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3079 - function resolveEmailState(botId) {
3080 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3081 - if (mxchatChat.initial_email_state.show_email_form) {
3082 - showEmailFormForBot(botId);
3083 - } else {
3084 - showChatContainerForBot(botId);
3085 - }
3086 - } else {
3087 - checkSessionAndEmailForBot(botId);
3088 - }
3089 - }
2549 + // MAIN FORM SUBMIT HANDLER
2550 + // Remove any existing event listeners first
2551 + emailForm.removeEventListener('submit', handleFormSubmit);
3090 2552
3091 - function checkSessionAndEmailForBot(botId) {
3092 - const sessionId = MxChatInstances.ensureSession(botId);
2553 + // Add the form submit handler
2554 + emailForm.addEventListener('submit', handleFormSubmit);
3093 2555
3094 - // Hide both panels while we check — show loader instead
3095 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3096 - var chatContainer = getElementDOM(botId, 'chat-container');
3097 - if (emailBlocker) emailBlocker.style.display = 'none';
3098 - if (chatContainer) chatContainer.style.display = 'none';
3099 - showInitLoader(botId);
2556 + function handleFormSubmit(event) {
2557 + event.preventDefault();
2558 + event.stopPropagation();
3100 2559
3101 - fetch(mxchatChat.ajax_url, {
3102 - method: 'POST',
3103 - headers: {
3104 - 'Content-Type': 'application/x-www-form-urlencoded',
3105 - },
3106 - body: new URLSearchParams({
3107 - action: 'mxchat_check_email_provided',
3108 - session_id: sessionId,
3109 - nonce: mxchatChat.nonce,
3110 - })
3111 - })
3112 - .then((response) => {
3113 - if (!response.ok) {
3114 - throw new Error(`HTTP error! status: ${response.status}`);
2560 + // Prevent double submission
2561 + if (isSubmitting) {
2562 + return false;
3115 2563 }
3116 - return response.json();
3117 - })
3118 - .then((data) => {
3119 - if (data.success) {
3120 - if (data.data.logged_in || data.data.email) {
3121 - showChatContainerForBot(botId);
3122 - } else {
3123 - showEmailFormForBot(botId);
3124 - }
3125 - } else {
3126 - showEmailFormForBot(botId);
3127 - }
3128 - })
3129 - .catch((error) => {
3130 - showEmailFormForBot(botId);
3131 - });
3132 - }
3133 2564
3134 - // Event delegation for email form submission
3135 - $(document).on('submit', '.email-collection-form', function(e) {
3136 - e.preventDefault();
3137 - e.stopPropagation();
2565 + const userEmail = document.getElementById('user-email').value.trim();
2566 + const nameInput = document.getElementById('user-name');
2567 + const userName = nameInput ? nameInput.value.trim() : '';
2568 + const sessionId = getChatSession();
3138 2569
3139 - var botId = getBotIdFromElement(this);
2570 + // Validate email before submission
2571 + if (!userEmail) {
2572 + showEmailError('Please enter your email address.');
2573 + return false;
2574 + }
3140 2575
3141 - // Prevent double submission
3142 - if (emailSubmittingState[botId]) {
3143 - return false;
3144 - }
2576 + if (!isValidEmail(userEmail)) {
2577 + showEmailError('Please enter a valid email address.');
2578 + return false;
2579 + }
3145 2580
3146 - var emailInput = getElementDOM(botId, 'user-email');
3147 - var nameInput = getElementDOM(botId, 'user-name');
3148 - var userEmail = emailInput ? emailInput.value.trim() : '';
3149 - var userName = nameInput ? nameInput.value.trim() : '';
3150 - var sessionId = MxChatInstances.ensureSession(botId);
2581 + // Validate name if field exists
2582 + if (nameInput && !isValidName(userName)) {
2583 + showEmailError('Please enter a valid name (2-100 characters).');
2584 + return false;
2585 + }
3151 2586
3152 - // Validate email
3153 - if (!userEmail) {
3154 - showEmailError(botId, 'Please enter your email address.');
3155 - return false;
3156 - }
2587 + // Clear any existing errors
2588 + clearEmailError();
2589 + setSubmissionState(true);
3157 2590
3158 - if (!isValidEmailAddress(userEmail)) {
3159 - showEmailError(botId, 'Please enter a valid email address.');
3160 - return false;
3161 - }
2591 + // Prepare form data with optional name
2592 + const formData = new URLSearchParams({
2593 + action: 'mxchat_handle_save_email_and_response',
2594 + email: userEmail,
2595 + session_id: sessionId,
2596 + nonce: mxchatChat.nonce,
2597 + });
3162 2598
3163 - // Validate name if field exists and has content
3164 - if (nameInput && userName && !isValidNameInput(userName)) {
3165 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3166 - return false;
3167 - }
2599 + // Add name to form data if provided
2600 + if (userName) {
2601 + formData.append('name', userName);
2602 + }
3168 2603
3169 - clearEmailError(botId);
3170 - setEmailSubmissionState(botId, true);
2604 + fetch(mxchatChat.ajax_url, {
2605 + method: 'POST',
2606 + headers: {
2607 + 'Content-Type': 'application/x-www-form-urlencoded',
2608 + },
2609 + body: formData
2610 + })
2611 + .then((response) => {
2612 + if (!response.ok) {
2613 + throw new Error(`HTTP error! status: ${response.status}`);
2614 + }
2615 + return response.json();
2616 + })
2617 + .then((data) => {
2618 + setSubmissionState(false);
3171 2619
3172 - // Prepare form data
3173 - const formData = new URLSearchParams({
3174 - action: 'mxchat_handle_save_email_and_response',
3175 - email: userEmail,
3176 - session_id: sessionId,
3177 - nonce: mxchatChat.nonce,
3178 - });
2620 + if (data.success) {
2621 + // Show chat immediately
2622 + showChatContainer();
3179 2623
3180 - if (userName) {
3181 - formData.append('name', userName);
2624 + // Handle bot response if provided
2625 + if (data.message && typeof appendMessage === 'function') {
2626 + setTimeout(() => {
2627 + appendMessage('bot', data.message);
2628 + if (typeof scrollToBottom === 'function') {
2629 + scrollToBottom();
2630 + }
2631 + }, 100);
2632 + }
2633 + } else {
2634 + showEmailError(data.message || 'Failed to save email. Please try again.');
2635 + }
2636 + })
2637 + .catch((error) => {
2638 + setSubmissionState(false);
2639 + showEmailError('An error occurred. Please try again.');
2640 + });
2641 +
2642 + return false; // Extra prevention
3182 2643 }
3183 2644
3184 - fetch(mxchatChat.ajax_url, {
3185 - method: 'POST',
3186 - headers: {
3187 - 'Content-Type': 'application/x-www-form-urlencoded',
3188 - },
3189 - body: formData
3190 - })
3191 - .then((response) => {
3192 - if (!response.ok) {
3193 - throw new Error(`HTTP error! status: ${response.status}`);
3194 - }
3195 - return response.json();
3196 - })
3197 - .then((data) => {
3198 - setEmailSubmissionState(botId, false);
2645 + // Real-time email validation
2646 + const emailInput = document.getElementById('user-email');
2647 + if (emailInput) {
2648 + let validationTimeout;
2649 +
2650 + emailInput.addEventListener('input', function() {
2651 + // Clear previous validation timeout
2652 + if (validationTimeout) {
2653 + clearTimeout(validationTimeout);
2654 + }
2655 +
2656 + // Debounce validation
2657 + validationTimeout = setTimeout(() => {
2658 + const email = this.value.trim();
2659 + clearEmailError();
2660 +
2661 + if (email && !isValidEmail(email)) {
2662 + showEmailError('Please enter a valid email address.');
2663 + }
2664 + }, 500);
2665 + });
3199 2666
3200 - if (data.success) {
3201 - showChatContainerForBot(botId);
2667 + // Handle Enter key
2668 + emailInput.addEventListener('keypress', function(e) {
2669 + if (e.key === 'Enter' && !isSubmitting) {
2670 + e.preventDefault();
2671 + emailForm.dispatchEvent(new Event('submit'));
2672 + }
2673 + });
2674 + }
3202 2675
3203 - // Replace {visitor_name} placeholder in intro message with actual name
3204 - if (userName) {
3205 - replaceVisitorNamePlaceholder(botId, userName);
3206 - } else {
3207 - // Remove placeholder if no name provided
3208 - replaceVisitorNamePlaceholder(botId, '');
2676 + // Real-time name validation
2677 + const nameInput = document.getElementById('user-name');
2678 + if (nameInput) {
2679 + let nameValidationTimeout;
2680 +
2681 + nameInput.addEventListener('input', function() {
2682 + // Clear previous validation timeout
2683 + if (nameValidationTimeout) {
2684 + clearTimeout(nameValidationTimeout);
3209 2685 }
2686 +
2687 + // Debounce validation
2688 + nameValidationTimeout = setTimeout(() => {
2689 + const name = this.value.trim();
2690 + clearEmailError();
2691 +
2692 + if (name && !isValidName(name)) {
2693 + showEmailError('Name must be between 2 and 100 characters.');
2694 + }
2695 + }, 500);
2696 + });
3210 2697
3211 - if (data.message && typeof appendMessage === 'function') {
3212 - setTimeout(() => {
3213 - appendMessage('bot', data.message, '', [], false, botId);
3214 - if (typeof scrollToBottom === 'function') {
3215 - scrollToBottom(botId);
3216 - }
3217 - }, 100);
2698 + // Handle Enter key
2699 + nameInput.addEventListener('keypress', function(e) {
2700 + if (e.key === 'Enter' && !isSubmitting) {
2701 + e.preventDefault();
2702 + emailForm.dispatchEvent(new Event('submit'));
3218 2703 }
2704 + });
2705 + }
2706 +
2707 + // Initial state check
2708 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2709 + const emailState = mxchatChat.initial_email_state;
2710 + if (emailState.show_email_form) {
2711 + showEmailForm();
3219 2712 } else {
3220 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2713 + showChatContainer();
3221 2714 }
3222 - })
3223 - .catch((error) => {
3224 - setEmailSubmissionState(botId, false);
3225 - showEmailError(botId, 'An error occurred. Please try again.');
3226 - });
3227 -
3228 - return false;
3229 - });
3230 -
3231 - // Real-time email validation using event delegation
3232 - $(document).on('input', '.mxchat-email-input', function() {
3233 - var botId = getBotIdFromElement(this);
3234 - var $input = $(this);
3235 -
3236 - // Clear previous timeout
3237 - clearTimeout($input.data('validationTimeout'));
3238 -
3239 - // Debounce validation
3240 - var timeout = setTimeout(() => {
3241 - var email = this.value.trim();
3242 - clearEmailError(botId);
3243 -
3244 - if (email && !isValidEmailAddress(email)) {
3245 - showEmailError(botId, 'Please enter a valid email address.');
3246 - }
3247 - }, 500);
3248 -
3249 - $input.data('validationTimeout', timeout);
3250 - });
3251 -
3252 - // Handle Enter key in email input
3253 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3254 - if (e.key === 'Enter') {
3255 - e.preventDefault();
3256 - var botId = getBotIdFromElement(this);
3257 - if (!emailSubmittingState[botId]) {
3258 - $(this).closest('.email-collection-form').submit();
3259 - }
2715 + } else {
2716 + // Check email status via AJAX
2717 + setTimeout(checkSessionAndEmail, 100);
3260 2718 }
3261 - });
3262 2719
3263 - // Handle Enter key in name input
3264 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3265 - if (e.key === 'Enter') {
3266 - e.preventDefault();
3267 - var botId = getBotIdFromElement(this);
3268 - if (!emailSubmittingState[botId]) {
3269 - $(this).closest('.email-collection-form').submit();
3270 - }
2720 + // Check if email exists for the current session
2721 + function checkSessionAndEmail() {
2722 + const sessionId = getChatSession();
2723 +
2724 + fetch(mxchatChat.ajax_url, {
2725 + method: 'POST',
2726 + headers: {
2727 + 'Content-Type': 'application/x-www-form-urlencoded',
2728 + },
2729 + body: new URLSearchParams({
2730 + action: 'mxchat_check_email_provided',
2731 + session_id: sessionId,
2732 + nonce: mxchatChat.nonce,
2733 + })
2734 + })
2735 + .then((response) => {
2736 + if (!response.ok) {
2737 + throw new Error(`HTTP error! status: ${response.status}`);
2738 + }
2739 + return response.json();
2740 + })
2741 + .then((data) => {
2742 + if (data.success) {
2743 + if (data.data.logged_in || data.data.email) {
2744 + showChatContainer();
2745 + } else {
2746 + showEmailForm();
2747 + }
2748 + } else {
2749 + // On error, default to showing email form
2750 + showEmailForm();
2751 + }
2752 + })
2753 + .catch((error) => {
2754 + // Email check failed - default to email form
2755 + showEmailForm();
2756 + });
3271 2757 }
3272 - });
3273 2758
3274 - // Initialize email check for all bot instances
3275 - // For floating bots: defer until widget is opened (zero passive AJAX)
3276 - // For embedded bots: check immediately since the form is visible
3277 - $('.mxchat-chatbot-wrapper').each(function() {
3278 - var botId = $(this).data('bot-id') || 'default';
3279 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3280 -
3281 - if (emailBlocker) {
3282 - if (isEmbeddedBot(botId)) {
3283 - // Embedded bots are always visible — check now
3284 - resolveEmailState(botId);
3285 - }
3286 - // Floating bots: handled in the widget open handler
3287 - } else if (isEmbeddedBot(botId)) {
3288 - // Embedded bot, no email collection — load history with loader
3289 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3290 - if (chatPersistenceEnabled) {
3291 - MxChatInstances.ensureSession(botId);
3292 - showChatContainerForBot(botId);
3293 - }
3294 - }
3295 - });
2759 + } else {
2760 + // Email collection is enabled but essential elements are missing - silently continue
2761 + }
3296 2762 }
3297 2763
3298 - // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3299 - $(document).on('click', '.pre-chat-message', function() {
3300 - var botId = getBotIdFromElement(this);
3301 - var $chatbot = getElement(botId, 'floating-chatbot');
3302 - if ($chatbot.hasClass('hidden')) {
3303 - $chatbot.removeClass('hidden').addClass('visible');
3304 - getElement(botId, 'floating-chatbot-button').addClass('hidden');
3305 - handlePreChatDismissal(botId);
2764 + // Open chatbot when pre-chat message is clicked
2765 + $(document).on('click', '#pre-chat-message', function() {
2766 + var chatbot = $('#floating-chatbot');
2767 + if (chatbot.hasClass('hidden')) {
2768 + chatbot.removeClass('hidden').addClass('visible');
2769 + $('#floating-chatbot-button').addClass('hidden');
2770 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
3306 2771 disableScroll(); // Disable scroll when chatbot opens
3307 -
3308 - // Load chat history for returning visitors (persistence)
3309 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3310 - if (chatPersistenceEnabled) {
3311 - MxChatInstances.ensureSession(botId);
3312 - }
3313 -
3314 - // Deferred email check — only on first widget open
3315 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3316 - var instance = MxChatInstances.get(botId);
3317 - if (emailBlocker && !instance.emailCheckDone) {
3318 - instance.emailCheckDone = true;
3319 - resolveEmailState(botId);
3320 - } else if (!emailBlocker) {
3321 - showChatContainerForBot(botId);
3322 - }
3323 2772 }
3324 2773 });
3325 2774
3326 - // Legacy duplicate close handler removed — handled by single event delegation above
2775 + var closeButton = document.querySelector('.close-pre-chat-message');
2776 + if (closeButton) {
2777 + closeButton.addEventListener('click', function() {
2778 + $('#pre-chat-message').fadeOut(200); // Hide the message
3327 2779
3328 -
3329 -function hasQuickQuestions(botId) {
3330 - botId = botId || 'default';
3331 - var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3332 - if (!questionsContainer) return false;
3333 - const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
2780 + // Send an AJAX request to set the transient flag for 24 hours
2781 + $.ajax({
2782 + url: mxchatChat.ajax_url,
2783 + type: 'POST',
2784 + data: {
2785 + action: 'mxchat_dismiss_pre_chat_message',
2786 + _ajax_nonce: mxchatChat.nonce
2787 + },
2788 + success: function() {
2789 + // Ensure the message is hidden after dismissal
2790 + $('#pre-chat-message').hide();
2791 + },
2792 + error: function() {
2793 + // Error dismissing pre-chat message - silently continue
2794 + }
2795 + });
2796 + });
2797 + }
2798 +
2799 +
2800 +function hasQuickQuestions() {
2801 + const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
3334 2802 return questionButtons.length > 0;
3335 2803 }
3336 2804
3337 -/**
3338 - * Check if a bot is embedded (not floating)
3339 - * Embedded bots don't have a .floating-chatbot wrapper
3340 - */
3341 -function isEmbeddedBot(botId) {
3342 - botId = botId || 'default';
3343 - var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3344 - return !floatingWrapper;
3345 -}
3346 -
3347 -function collapseQuickQuestions(botId) {
3348 - botId = botId || 'default';
3349 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3350 - if (questionsContainer && hasQuickQuestions(botId)) {
2805 +function collapseQuickQuestions() {
2806 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2807 + if (questionsContainer && hasQuickQuestions()) {
3351 2808 questionsContainer.classList.add('collapsed');
3352 2809 questionsContainer.classList.add('has-been-collapsed');
3353 2810 try {
3354 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3355 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
2811 + sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2812 + sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
3356 2813 } catch (e) {
3357 2814 // Ignore if sessionStorage is not available
3358 2815 }
3359 2816 }
@@ -3358,15 +2815,14 @@
3358 2815 }
3359 2816 }
3360 2817 }
3361 2818
3362 -function expandQuickQuestions(botId) {
3363 - botId = botId || 'default';
3364 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3365 - if (questionsContainer && hasQuickQuestions(botId)) {
2819 +function expandQuickQuestions() {
2820 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2821 + if (questionsContainer && hasQuickQuestions()) {
3366 2822 questionsContainer.classList.remove('collapsed');
3367 2823 try {
3368 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
2824 + sessionStorage.setItem('mxchat_questions_collapsed', 'false');
3369 2825 } catch (e) {
3370 2826 // Ignore if sessionStorage is not available
3371 2827 }
3372 2828 }
@@ -3371,24 +2827,18 @@
3371 2827 }
3372 2828 }
3373 2829 }
3374 2830
3375 -function checkQuickQuestionsState(botId) {
3376 - botId = botId || 'default';
3377 - if (!hasQuickQuestions(botId)) {
2831 +function checkQuickQuestionsState() {
2832 + if (!hasQuickQuestions()) {
3378 2833 return; // Don't do anything if no questions exist
3379 2834 }
3380 -
3381 - // Skip restoring collapsed state for embedded bots - they should always start expanded
3382 - if (isEmbeddedBot(botId)) {
3383 - return;
3384 - }
3385 -
2835 +
3386 2836 try {
3387 - const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3388 - const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3389 -
3390 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
2837 + const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2838 + const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2839 +
2840 + const questionsContainer = document.getElementById('mxchat-popular-questions');
3391 2841 if (questionsContainer) {
3392 2842 if (hasBeenCollapsed === 'true') {
3393 2843 questionsContainer.classList.add('has-been-collapsed');
3394 2844 }
@@ -3401,36 +2851,32 @@
3401 2851 }
3402 2852 }
3403 2853
3404 2854 // Global delegation for dynamically added links as fallback
3405 -// Use class selector for multi-instance support
3406 -$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
2855 +$(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
3407 2856 const $link = $(this);
3408 2857 const messageDiv = $link.closest('.bot-message, .agent-message');
3409 -
2858 +
3410 2859 // Only process bot/agent message links
3411 2860 if (messageDiv.length > 0) {
3412 2861 const originalHref = $link.attr('href');
3413 -
2862 +
3414 2863 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3415 2864 e.preventDefault();
3416 2865 e.stopPropagation();
3417 -
2866 +
3418 2867 // Mark as tracked
3419 2868 $link.attr('data-tracked', 'true');
3420 -
3421 - // Get bot ID from the chat box context
3422 - var botId = getBotIdFromElement(this);
3423 -
2869 +
3424 2870 // Get message context from the message div
3425 2871 const messageText = messageDiv.text().substring(0, 200);
3426 -
2872 +
3427 2873 $.ajax({
3428 2874 url: mxchatChat.ajax_url,
3429 2875 type: 'POST',
3430 2876 data: {
3431 2877 action: 'mxchat_track_url_click',
3432 - session_id: getChatSession(botId),
2878 + session_id: getChatSession(),
3433 2879 url: originalHref,
3434 2880 message_context: messageText,
3435 2881 nonce: mxchatChat.nonce
3436 2882 },
@@ -3441,57 +2887,49 @@
3441 2887 window.location.href = originalHref;
3442 2888 }
3443 2889 }
3444 2890 });
3445 -
2891 +
3446 2892 return false;
3447 2893 }
3448 2894 }
3449 2895 });
3450 2896
3451 - // ====================================
3452 - // MAIN INITIALIZATION
3453 - // ====================================
2897 +
2898 +
2899 +
2900 +// ====================================
2901 +// MAIN INITIALIZATION
2902 +// ====================================
3454 2903
3455 - // Initialize all chatbot instances on the page
3456 - initializeAllInstances();
2904 +if ($('#floating-chatbot').hasClass('hidden')) {
2905 + $('#floating-chatbot-button').removeClass('hidden');
2906 +}
2907 +// Initialize when document is ready
2908 +setFullHeight();
2909 +trackOriginatingPage();
3457 2910
3458 - // Legacy initialization for single bot compatibility
3459 - $('.floating-chatbot.hidden').each(function() {
3460 - var botId = getBotIdFromElement(this);
3461 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3462 - });
2911 +// Only load chat history if email collection is disabled
2912 +if (mxchatChat.email_collection_enabled !== 'on') {
2913 + loadChatHistory();
2914 +}
3463 2915
3464 - // Initialize when document is ready
3465 - setFullHeight();
2916 +initializeChatVisibility();
3466 2917
3467 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3468 - // until the user's first interaction via MxChatInstances.ensureSession()
3469 -
3470 - // Initialize chat visibility for all instances
3471 - $('.mxchat-chatbot-wrapper').each(function() {
3472 - var botId = $(this).data('bot-id') || 'default';
3473 - initializeChatVisibility(botId);
3474 - });
3475 -
3476 - // Make functions globally available for add-ons
3477 - window.hasQuickQuestions = hasQuickQuestions;
3478 - window.collapseQuickQuestions = collapseQuickQuestions;
3479 - window.appendMessage = appendMessage;
3480 - window.appendThinkingMessage = appendThinkingMessage;
3481 - window.scrollToBottom = scrollToBottom;
3482 - window.scrollElementToTop = scrollElementToTop;
3483 - window.replaceLastMessage = replaceLastMessage;
3484 - window.callMxChat = callMxChat;
3485 - window.callMxChatStream = callMxChatStream;
3486 - window.shouldUseStreaming = shouldUseStreaming;
3487 - window.getChatSession = getChatSession;
3488 - window.getPageContext = getPageContext;
3489 - window.updateStreamingMessage = updateStreamingMessage;
3490 - window.MxChatInstances = MxChatInstances;
3491 - window.getElement = getElement;
3492 - window.getElementDOM = getElementDOM;
3493 - window.getBotIdFromElement = getBotIdFromElement;
2918 +// Make functions globally available for add-ons
2919 +window.hasQuickQuestions = hasQuickQuestions;
2920 +window.collapseQuickQuestions = collapseQuickQuestions;
2921 +window.appendMessage = appendMessage;
2922 +window.appendThinkingMessage = appendThinkingMessage;
2923 +window.scrollToBottom = scrollToBottom;
2924 +window.scrollElementToTop = scrollElementToTop;
2925 +window.replaceLastMessage = replaceLastMessage;
2926 +window.callMxChat = callMxChat;
2927 +window.callMxChatStream = callMxChatStream;
2928 +window.shouldUseStreaming = shouldUseStreaming;
2929 +window.getChatSession = getChatSession;
2930 +window.getPageContext = getPageContext;
2931 +window.updateStreamingMessage = updateStreamingMessage;
3494 2932
3495 2933 }); // End of jQuery ready
3496 2934
3497 2935