PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.5
MxChat – AI Chatbot & Content Generation for WordPress v2.3.5
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 +1064 -2118 3.2.22.3.5 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,53 @@
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;
15 +
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;
218 25
219 - var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
220 26
221 27 // ====================================
222 - // SESSION MANAGEMENT (Legacy compatibility)
28 + // SESSION MANAGEMENT
223 29 // ====================================
224 -
30 +
31 + function getChatSession() {
32 + var sessionId = getCookie('mxchat_session_id');
33 + //console.log("Session ID retrieved from cookie: ", sessionId);
34 +
35 + if (!sessionId) {
36 + sessionId = generateSessionId();
37 + //console.log("Generated new session ID: ", sessionId);
38 + setChatSession(sessionId);
39 + }
40 +
41 + //console.log("Final session ID: ", sessionId);
42 + return sessionId;
43 + }
44 +
45 + function setChatSession(sessionId) {
46 + // Set the cookie with a 24-hour expiration (86400 seconds)
47 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
48 + }
49 +
225 50 function getCookie(name) {
226 51 let value = "; " + document.cookie;
227 52 let parts = value.split("; " + name + "=");
228 53 if (parts.length == 2) return parts.pop().split(";").shift();
229 54 }
230 -
55 +
231 56 function generateSessionId() {
232 57 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
233 58 }
234 59
235 - // Legacy function - now delegates to instance manager
236 - function getChatSession(botId) {
237 - botId = botId || 'default';
238 - return MxChatInstances.getChatSession(botId);
239 - }
240 -
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 60 // ====================================
275 61 // CONTEXTUAL AWARENESS FUNCTIONALITY
276 62 // ====================================
277 63
@@ -347,9 +133,9 @@
347 133 const elements = clone.querySelectorAll(selector);
348 134 elements.forEach(el => el.remove());
349 135 });
350 136
351 - // Extract MxChat context data attributes before getting text content
137 + // NEW: Extract MxChat context data attributes before getting text content
352 138 const contextData = [];
353 139 clone.querySelectorAll('[data-mxchat-context]').forEach(el => {
354 140 const contextValue = el.dataset.mxchatContext;
355 141 if (contextValue && contextValue.trim()) {
@@ -383,9 +169,9 @@
383 169 content: pageContent
384 170 };
385 171 }
386 172
387 -// Track originating page when chat starts
173 +// NEW: Track originating page when chat starts
388 174 function trackOriginatingPage() {
389 175 const sessionId = getChatSession();
390 176 const pageUrl = window.location.href;
391 177 const pageTitle = document.title || 'Untitled Page';
@@ -416,123 +202,70 @@
416 202
417 203 // ====================================
418 204 // CORE CHAT FUNCTIONALITY
419 205 // ====================================
420 -
421 -// 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');
426 - if (chatInput) {
427 - chatInput.disabled = true;
428 - chatInput.style.opacity = '0.6';
429 - }
430 - if (sendButton) {
431 - sendButton.disabled = true;
432 - sendButton.style.opacity = '0.5';
433 - sendButton.style.pointerEvents = 'none';
434 - }
435 -}
436 -
437 -function enableChatInput(botId) {
438 - botId = botId || 'default';
439 - var chatInput = getElementDOM(botId, 'chat-input');
440 - var sendButton = getElementDOM(botId, 'send-button');
441 - if (chatInput) {
442 - chatInput.disabled = false;
443 - chatInput.style.opacity = '1';
444 - chatInput.focus();
445 - }
446 - if (sendButton) {
447 - sendButton.disabled = false;
448 - sendButton.style.opacity = '1';
449 - sendButton.style.pointerEvents = 'auto';
450 - }
451 -}
452 -
453 206 // 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();
459 -
207 +function sendMessage() {
208 + var message = $('#chat-input').val();
209 +
460 210 // ADD PROMPT HOOK HERE
461 - if (typeof customMxChatFilter === 'function') {
462 - message = customMxChatFilter(message, "prompt");
211 + if (typeof customMxChatFilter === 'function') {
212 + message = customMxChatFilter(message, "prompt");
463 213 }
464 -
214 +
465 215 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 - }
216 + appendMessage("user", message);
217 + $('#chat-input').val('');
218 + $('#chat-input').css('height', 'auto');
472 219
473 - appendMessage("user", message, '', [], false, botId);
474 - $chatInput.val('');
475 - $chatInput.css('height', 'auto');
476 -
477 - if (hasQuickQuestions(botId)) {
478 - collapseQuickQuestions(botId);
220 + if (hasQuickQuestions()) {
221 + collapseQuickQuestions();
479 222 }
480 - appendThinkingMessage(botId);
481 - scrollToBottom(botId);
223 + appendThinkingMessage();
224 + scrollToBottom();
482 225
483 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
226 + const currentModel = mxchatChat.model || 'gpt-4o';
484 227
485 228 // Check if streaming is enabled AND supported for this model
486 229 if (shouldUseStreaming(currentModel)) {
487 230 callMxChatStream(message, function(response) {
488 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
489 - }, botId);
231 + $('.bot-message.temporary-message').removeClass('temporary-message');
232 + });
490 233 } else {
491 234 callMxChat(message, function(response) {
492 - replaceLastMessage("bot", response, '', [], botId);
493 - }, botId);
235 + replaceLastMessage("bot", response);
236 + });
494 237 }
495 238 }
496 239 }
497 240
498 241 // Update your existing sendMessageToChatbot function
499 -function sendMessageToChatbot(message, botId) {
500 - botId = botId || 'default';
501 - MxChatInstances.ensureSession(botId);
502 -
242 +function sendMessageToChatbot(message) {
503 243 // ADD PROMPT HOOK HERE
504 244 if (typeof customMxChatFilter === 'function') {
505 245 message = customMxChatFilter(message, "prompt");
506 246 }
247 +
248 + var sessionId = getChatSession();
507 249
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);
250 + if (hasQuickQuestions()) {
251 + collapseQuickQuestions();
513 252 }
253 + appendThinkingMessage();
254 + scrollToBottom();
514 255
515 - var sessionId = getChatSession(botId);
256 + const currentModel = mxchatChat.model || 'gpt-4o';
516 257
517 - if (hasQuickQuestions(botId)) {
518 - collapseQuickQuestions(botId);
519 - }
520 - appendThinkingMessage(botId);
521 - scrollToBottom(botId);
522 -
523 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
524 -
525 258 // Check if streaming is enabled AND supported for this model
526 259 if (shouldUseStreaming(currentModel)) {
527 260 callMxChatStream(message, function(response) {
528 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
529 - }, botId);
261 + $('.bot-message.temporary-message').removeClass('temporary-message');
262 + });
530 263 } else {
531 264 callMxChat(message, function(response) {
532 - getElement(botId, 'chat-box').find('.temporary-message').remove();
533 - replaceLastMessage("bot", response, '', [], botId);
534 - }, botId);
265 + $('.temporary-message').remove();
266 + replaceLastMessage("bot", response);
267 + });
535 268 }
536 269 }
537 270
538 271 // Updated shouldUseStreaming function with debugging
@@ -547,71 +280,20 @@
547 280 // Only use streaming if both enabled and supported
548 281 return streamingEnabled && streamingSupported;
549 282 }
550 283
551 -// Helper function to handle chat mode updates
552 -function handleChatModeUpdates(response, responseText) {
553 - // Check for explicit chat mode in response (THIS IS THE KEY FIX)
554 - if (response.chat_mode) {
555 - updateChatModeIndicator(response.chat_mode);
556 - return; // Return early since we found explicit mode
557 - }
558 - // Check for fallback response chat mode
559 - else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
560 - updateChatModeIndicator(response.fallbackResponse.chat_mode);
561 - return; // Return early since we found explicit mode
562 - }
563 -
564 - // Only do text-based detection if no explicit mode was provided
565 - // Check for specific AI chatbot response text
566 - if (responseText === 'You are now chatting with the AI chatbot.' ||
567 - responseText.includes('now chatting with the AI') ||
568 - responseText.includes('switched to AI mode') ||
569 - responseText.includes('AI chatbot is now')) {
570 - updateChatModeIndicator('ai');
571 - }
572 - // Check for agent transfer messages
573 - else if (responseText.includes('agent') &&
574 - (responseText.includes('transfer') || responseText.includes('connected'))) {
575 - updateChatModeIndicator('agent');
576 - }
577 -}
578 -
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');
588 - return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
589 -}
590 -
591 -function callMxChat(message, callback, botId) {
592 - botId = botId || getMxChatBotId();
593 -
594 - // 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);
596 -
284 +function callMxChat(message, callback) {
597 285 // Get page context if contextual awareness is enabled
598 286 const pageContext = getPageContext();
599 -
600 - // Get instance for session start timestamp (used when persistence is OFF)
601 - var instance = MxChatInstances.get(botId);
602 -
287 +
603 288 // Prepare AJAX data
604 289 const ajaxData = {
605 290 action: 'mxchat_handle_chat_request',
606 291 message: message,
607 - session_id: getChatSession(botId),
292 + session_id: getChatSession(),
608 293 nonce: mxchatChat.nonce,
609 - current_page_url: window.location.href,
610 - 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
294 + current_page_url: window.location.href,
295 + current_page_title: document.title
614 296 };
615 297
616 298 // Add page context if available
617 299 if (pageContext) {
@@ -634,98 +316,37 @@
634 316 type: 'POST',
635 317 dataType: 'json',
636 318 data: ajaxData,
637 319 success: function(response) {
638 - // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
639 - if (response.chat_mode) {
640 - updateChatModeIndicator(response.chat_mode, botId);
641 - }
320 + // Log the full response for debugging
321 + //console.log("API Response:", response);
642 322
643 - // Also check in data property if response is wrapped
644 - if (response.data && response.data.chat_mode) {
645 - updateChatModeIndicator(response.data.chat_mode, botId);
646 - }
647 -
648 - // SECURITY FIX: Check for errors FIRST before checking for success
649 - // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
650 - if (response.success === false || (response.data && response.data.error_message)) {
651 - let errorMessage = "";
652 - let errorCode = "";
653 -
654 - // Check various possible error locations in the response
655 - if (response.data && response.data.error_message) {
656 - errorMessage = response.data.error_message;
657 - errorCode = response.data.error_code || "";
658 - } else if (response.error_message) {
659 - errorMessage = response.error_message;
660 - errorCode = response.error_code || "";
661 - } else if (response.message) {
662 - errorMessage = response.message;
663 - } else if (typeof response.data === 'string') {
664 - errorMessage = response.data;
665 - } else {
666 - // Fallback for any other unexpected response format
667 - errorMessage = "An error occurred. Please try again or contact support.";
668 - }
669 -
670 - // Handle session reset action (IP changed, session expired, etc.)
671 - // Silent reset — keep chat UI intact, just get a new session and retry
672 - 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');
676 - 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';
679 - if (shouldUseStreaming(currentModel)) {
680 - callMxChatStream(originalMessage, function(response) {
681 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
682 - }, botId);
683 - } else {
684 - callMxChat(originalMessage, function(response) {
685 - replaceLastMessage("bot", response, '', [], botId);
686 - }, botId);
687 - }
688 - }
689 - return;
690 - }
691 -
692 - // Format user-friendly error message
693 - let displayMessage = errorMessage;
694 -
695 - // Customize message for admin users
696 - if (mxchatChat.is_admin) {
697 - // For admin users, show more technical details including error code
698 - displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
699 - }
700 -
701 - replaceLastMessage("bot", displayMessage, '', [], botId);
702 - return; // Exit early for errors
703 - }
704 -
705 - // NOW check if this is a successful response by looking for text, html, or message fields
323 + // First check if this is a successful response by looking for text, html, or message fields
706 324 // This preserves compatibility with your server response format
707 - if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
325 + if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
708 326 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
709 327
710 328 // Handle successful response - this is your original success handling code
711 329
712 - // Handle other responses
713 - let responseText = response.text || '';
714 - let responseHtml = response.html || '';
715 - let responseMessage = response.message || '';
330 + // Existing chat mode check
331 + if (response.chat_mode) {
332 + updateChatModeIndicator(response.chat_mode);
333 + }
334 + else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
335 + updateChatModeIndicator(response.fallbackResponse.chat_mode);
336 + }
716 337
717 338 // Add PDF filename handling
718 339 if (response.data && response.data.filename) {
719 - showActivePdf(response.data.filename, botId);
720 - var instance = MxChatInstances.get(botId);
721 - instance.activePdfFile = response.data.filename;
340 + showActivePdf(response.data.filename);
341 + activePdfFile = response.data.filename;
722 342 }
723 343
724 344 // Add redirect check here
725 345 if (response.redirect_url) {
346 + let responseText = response.text || '';
726 347 if (responseText) {
727 - replaceLastMessage("bot", responseText, '', [], botId);
348 + replaceLastMessage("bot", responseText);
728 349 }
729 350 setTimeout(() => {
730 351 window.location.href = response.redirect_url;
731 352 }, 1500);
@@ -733,17 +354,24 @@
733 354 }
734 355
735 356 // Check for live agent response
736 357 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
737 - removeThinkingDots(botId);
738 - updateChatModeIndicator('agent', botId);
739 - enableChatInput(botId);
358 + updateChatModeIndicator('agent');
740 359 return;
741 360 }
742 361
362 + // Handle other responses
363 + let responseText = response.text || '';
364 + let responseHtml = response.html || '';
365 + let responseMessage = response.message || '';
366 +
367 + if (responseText === 'You are now chatting with the AI chatbot.') {
368 + updateChatModeIndicator('ai');
369 + }
370 +
743 371 // Handle the message and show notification if chat is hidden
744 372 if (responseText || responseHtml || responseMessage) {
745 -
373 +
746 374 // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
747 375 if (responseText && typeof customMxChatFilter === 'function') {
748 376 responseText = customMxChatFilter(responseText, "response");
749 377 }
@@ -749,53 +377,89 @@
749 377 }
750 378 if (responseMessage && typeof customMxChatFilter === 'function') {
751 379 responseMessage = customMxChatFilter(responseMessage, "response");
752 380 }
753 -
381 +
754 382 // Update the messages as before
755 383 if (responseText && responseHtml) {
756 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
384 + replaceLastMessage("bot", responseText, responseHtml);
757 385 } else if (responseText) {
758 - replaceLastMessage("bot", responseText, '', [], botId);
386 + replaceLastMessage("bot", responseText);
759 387 } else if (responseHtml) {
760 - replaceLastMessage("bot", "", responseHtml, [], botId);
388 + replaceLastMessage("bot", "", responseHtml);
761 389 } else if (responseMessage) {
762 - replaceLastMessage("bot", responseMessage, '', [], botId);
390 + replaceLastMessage("bot", responseMessage);
763 391 }
764 392
765 393 // 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();
394 + if ($('#floating-chatbot').hasClass('hidden')) {
395 + const badge = $('#chat-notification-badge');
396 + if (badge.length) {
397 + badge.show();
771 398 }
772 399 }
773 400 } 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);
401 + ////console.error("Unexpected response format:", response);
402 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
779 403 }
780 404
781 405 if (response.message_id) {
782 - var instance = MxChatInstances.get(botId);
783 - instance.lastSeenMessageId = response.message_id;
406 + lastSeenMessageId = response.message_id;
784 407 }
785 408
786 409 return;
787 410 }
788 411
789 - // Fallback for truly unexpected response formats
790 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
412 + // If we got here, it's likely an error response
413 + // Now we can check for error conditions with our robust error handling
414 +
415 + let errorMessage = "";
416 + let errorCode = "";
417 +
418 + // Check various possible error locations in the response
419 + if (response.data && response.data.error_message) {
420 + errorMessage = response.data.error_message;
421 + errorCode = response.data.error_code || "";
422 + } else if (response.error_message) {
423 + errorMessage = response.error_message;
424 + errorCode = response.error_code || "";
425 + } else if (response.message) {
426 + errorMessage = response.message;
427 + } else if (typeof response.data === 'string') {
428 + errorMessage = response.data;
429 + } else if (!response.success) {
430 + // Explicit check for success: false without other error info
431 + errorMessage = "An error occurred. Please try again or contact support.";
432 + } else {
433 + // Fallback for any other unexpected response format
434 + errorMessage = "Unexpected response received. Please try again or contact support.";
435 + }
436 +
437 + // Log the error with code for debugging
438 + //console.log("Response data:", response.data);
439 + ////console.error("API Error:", errorMessage, "Code:", errorCode);
440 +
441 + // Format user-friendly error message
442 + let displayMessage = errorMessage;
443 +
444 + // Customize message for admin users
445 + if (mxchatChat.is_admin) {
446 + // For admin users, show more technical details including error code
447 + displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
448 + }
449 +
450 + replaceLastMessage("bot", displayMessage);
791 451 },
792 452 error: function(xhr, status, error) {
453 + //console.error("AJAX Error:", status, error);
454 + //console.log("Response Text:", xhr.responseText);
455 +
793 456 let errorMessage = "An unexpected error occurred.";
794 457
795 458 // Try to parse the response if it's JSON
796 459 try {
797 460 const responseJson = JSON.parse(xhr.responseText);
461 + //console.log("Parsed error response:", responseJson);
798 462
799 463 if (responseJson.data && responseJson.data.error_message) {
800 464 errorMessage = responseJson.data.error_message;
801 465 } else if (responseJson.message) {
@@ -815,22 +479,20 @@
815 479 errorMessage = "Server error: The server encountered an issue. Please try again later.";
816 480 }
817 481 }
818 482
819 - replaceLastMessage("bot", errorMessage, '', [], botId);
483 + replaceLastMessage("bot", errorMessage);
820 484 }
821 485 });
822 486 }
823 487
824 -function callMxChatStream(message, callback, botId) {
825 - botId = botId || getMxChatBotId();
826 -
827 - // 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);
829 -
830 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
488 +function callMxChatStream(message, callback) {
489 + //console.log("Using streaming for message:", message);
490 +
491 + const currentModel = mxchatChat.model || 'gpt-4o';
831 492 if (!isStreamingSupported(currentModel)) {
832 - callMxChat(message, callback, botId);
493 + //console.log("Streaming not supported, falling back to regular call");
494 + callMxChat(message, callback);
833 495 return;
834 496 }
835 497
836 498 // Get page context if contextual awareness is enabled
@@ -835,22 +497,16 @@
835 497
836 498 // Get page context if contextual awareness is enabled
837 499 const pageContext = getPageContext();
838 500
839 - // Get instance for session start timestamp (used when persistence is OFF)
840 - var instance = MxChatInstances.get(botId);
841 -
842 501 const formData = new FormData();
843 502 formData.append('action', 'mxchat_stream_chat');
844 503 formData.append('message', message);
845 - formData.append('session_id', getChatSession(botId));
504 + formData.append('session_id', getChatSession());
846 505 formData.append('nonce', mxchatChat.nonce);
847 506 formData.append('current_page_url', window.location.href);
848 507 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 -
508 +
853 509 // Add page context if available
854 510 if (pageContext) {
855 511 formData.append('page_context', JSON.stringify(pageContext));
856 512 }
@@ -867,9 +523,8 @@
867 523 }
868 524
869 525 let accumulatedContent = '';
870 526 let testingDataReceived = false;
871 - let streamingStarted = false;
872 527
873 528 fetch(mxchatChat.ajax_url, {
874 529 method: 'POST',
875 530 body: formData,
@@ -875,40 +530,67 @@
875 530 body: formData,
876 531 credentials: 'same-origin'
877 532 })
878 533 .then(response => {
879 - // Store the response for potential fallback handling
880 - const responseClone = response.clone();
881 -
534 + //console.log("Streaming response received:", response);
535 +
882 536 if (!response.ok) {
883 - // Try to get error details from response
884 - return responseClone.json().then(errorData => {
885 - throw { isServerError: true, data: errorData };
886 - }).catch(() => {
887 - throw new Error('Network response was not ok');
888 - });
537 + throw new Error('Network response was not ok');
889 538 }
890 539
891 - // Check if response is JSON instead of streaming
892 - const contentType = response.headers.get('content-type');
893 - if (contentType && contentType.includes('application/json')) {
894 - return responseClone.json().then(data => {
895 - // IMMEDIATE CHAT MODE UPDATE for JSON response
896 - if (data.chat_mode) {
897 - updateChatModeIndicator(data.chat_mode, botId);
898 - }
540 + // Check if response is JSON instead of streaming
541 + const contentType = response.headers.get('content-type');
542 + if (contentType && contentType.includes('application/json')) {
543 + //console.log("Received JSON response instead of stream, handling as regular response");
544 + return response.json().then(data => {
899 545
900 - // Check for testing panel
901 - if (window.mxchatTestPanelInstance && data.testing_data) {
902 - window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
903 - }
546 + // FIXED: Always check for testing panel, not just in testing mode
547 + if (window.mxchatTestPanelInstance && data.testing_data) {
548 + //console.log('Testing data found in streaming JSON response:', data.testing_data);
549 + window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
550 + }
551 +
552 + // Handle as regular JSON response
553 + $('.bot-message.temporary-message').remove();
554 +
555 + // Handle different response formats (including intent responses)
556 + if (data.text || data.html || data.message) {
557 +
558 + // ADD RESPONSE HOOKS HERE - FOR STREAMING JSON RESPONSES
559 + if (data.text && typeof customMxChatFilter === 'function') {
560 + data.text = customMxChatFilter(data.text, "response");
561 + }
562 + if (data.message && typeof customMxChatFilter === 'function') {
563 + data.message = customMxChatFilter(data.message, "response");
564 + }
565 +
566 + if (data.text && data.html) {
567 + replaceLastMessage("bot", data.text, data.html);
568 + } else if (data.text) {
569 + replaceLastMessage("bot", data.text);
570 + } else if (data.html) {
571 + replaceLastMessage("bot", "", data.html);
572 + } else if (data.message) {
573 + replaceLastMessage("bot", data.message);
574 + }
575 + }
576 +
577 + // Handle other response properties
578 + if (data.chat_mode) {
579 + updateChatModeIndicator(data.chat_mode);
580 + }
581 +
582 + if (data.data && data.data.filename) {
583 + showActivePdf(data.data.filename);
584 + activePdfFile = data.data.filename;
585 + }
586 +
587 + if (callback) {
588 + callback(data.text || data.message || '');
589 + }
590 + });
591 + }
904 592
905 - // Handle the JSON response directly
906 - handleNonStreamResponse(data, callback, botId);
907 - return Promise.resolve(); // Prevent further processing
908 - });
909 - }
910 -
911 593 // Continue with streaming processing
912 594 const reader = response.body.getReader();
913 595 const decoder = new TextDecoder();
914 596 let buffer = '';
@@ -915,36 +597,9 @@
915 597
916 598 function processStream() {
917 599 reader.read().then(({ done, value }) => {
918 600 if (done) {
919 - // If streaming completed but no content was received, try to get response as fallback
920 - if (!streamingStarted || !accumulatedContent) {
921 - // Try to read the response as JSON
922 - responseClone.text().then(text => {
923 - try {
924 - const data = JSON.parse(text);
925 - if (data.text || data.message || data.html) {
926 - handleNonStreamResponse(data, callback, botId);
927 - } else {
928 - // No valid data, fall back to regular call
929 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
930 - callMxChat(message, callback, botId);
931 - }
932 - } catch (e) {
933 - // Could not parse, fall back to regular call
934 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
935 - callMxChat(message, callback, botId);
936 - }
937 - }).catch(() => {
938 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
939 - callMxChat(message, callback, botId);
940 - });
941 - return;
942 - }
943 -
944 - // Re-enable chat input when stream ends with content
945 - enableChatInput(botId);
946 -
601 + //console.log("Streaming completed, final content:", accumulatedContent);
947 602 if (callback) {
948 603 callback(accumulatedContent);
949 604 }
950 605 return;
@@ -958,17 +613,9 @@
958 613 if (line.startsWith('data: ')) {
959 614 const data = line.substring(6);
960 615
961 616 if (data === '[DONE]') {
962 - if (!accumulatedContent) {
963 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
964 - callMxChat(message, callback, botId);
965 - return;
966 - }
967 -
968 - // Re-enable chat input after streaming completes
969 - enableChatInput(botId);
970 -
617 + //console.log("Received [DONE] signal");
971 618 if (callback) {
972 619 callback(accumulatedContent);
973 620 }
974 621 return;
@@ -975,16 +622,12 @@
975 622 }
976 623
977 624 try {
978 625 const json = JSON.parse(data);
979 -
980 - // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
981 - if (json.chat_mode) {
982 - updateChatModeIndicator(json.chat_mode, botId);
983 - }
984 -
985 - // Handle testing data
626 +
627 + // FIXED: Always check for testing panel and handle testing data properly
986 628 if (json.testing_data && !testingDataReceived) {
629 + //console.log('Testing data received in stream:', json.testing_data);
987 630 if (window.mxchatTestPanelInstance) {
988 631 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
989 632 testingDataReceived = true;
990 633 }
@@ -990,297 +633,90 @@
990 633 }
991 634 }
992 635 // Handle content streaming
993 636 else if (json.content) {
994 - streamingStarted = true;
995 637 accumulatedContent += json.content;
996 - updateStreamingMessage(accumulatedContent, botId);
997 - }
998 - // Handle complete response in stream (fallback response)
999 - else if (json.text || json.message || json.html) {
1000 - handleNonStreamResponse(json, callback, botId);
1001 - return;
1002 - }
638 + updateStreamingMessage(accumulatedContent);
639 + }
1003 640 // Handle errors
1004 641 else if (json.error) {
1005 -
1006 - // Get error message from various possible fields
1007 - let errorMessage = json.error_message || json.message || json.text ||
1008 - (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1009 -
1010 - // Re-enable chat input on error
1011 - enableChatInput(botId);
1012 -
1013 - // Display the error directly in the chat
1014 - replaceLastMessage("bot", errorMessage, '', [], botId);
1015 -
1016 - if (callback) {
1017 - callback(errorMessage);
1018 - }
642 + //console.error("Streaming error:", json.error);
643 + replaceLastMessage("bot", "Error: " + json.error);
1019 644 return;
1020 645 }
1021 646 } catch (e) {
1022 - // SSE data parsing error - silently continue
647 + //console.error('Error parsing SSE data:', e, 'Data:', data);
1023 648 }
1024 649 }
1025 650 }
1026 651
1027 652 processStream();
1028 - }).catch(streamError => {
1029 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1030 - callMxChat(message, callback, botId);
1031 653 });
1032 654 }
1033 655
1034 656 processStream();
1035 657 })
1036 - .catch(error => {
1037 - // Check if we have server error data with chat mode
1038 - if (error && error.isServerError && error.data) {
1039 - // Check for chat mode in error data
1040 - if (error.data.chat_mode) {
1041 - updateChatModeIndicator(error.data.chat_mode, botId);
1042 - }
1043 -
1044 - handleNonStreamResponse(error.data, callback, botId);
1045 - } else {
1046 - // 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);
1049 - }
1050 - });
658 + .catch(error => {
659 + //console.error('Streaming error:', error);
660 + callMxChat(message, callback);
661 + });
1051 662 }
1052 663
1053 -// Helper function to handle non-streaming responses
1054 -function handleNonStreamResponse(data, callback, botId) {
1055 - botId = botId || 'default';
1056 -
1057 - // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1058 - if (data.chat_mode) {
1059 - updateChatModeIndicator(data.chat_mode, botId);
1060 - }
1061 -
1062 - // Also check in data property if response is wrapped
1063 - if (data.data && data.data.chat_mode) {
1064 - updateChatModeIndicator(data.data.chat_mode, botId);
1065 - }
1066 -
1067 - // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1068 - // This prevents a visual gap between thinking dots disappearing and content appearing
1069 -
1070 - // SECURITY FIX: Check for errors FIRST
1071 - if (data.success === false || (data.data && data.data.error_message)) {
1072 - let errorMessage = "";
1073 - let errorCode = "";
1074 -
1075 - // Check various possible error locations
1076 - if (data.data && data.data.error_message) {
1077 - errorMessage = data.data.error_message;
1078 - errorCode = data.data.error_code || "";
1079 - } else if (data.error_message) {
1080 - errorMessage = data.error_message;
1081 - errorCode = data.error_code || "";
1082 - } else if (data.message) {
1083 - errorMessage = data.message;
1084 - } else if (typeof data.data === 'string') {
1085 - errorMessage = data.data;
1086 - } else {
1087 - errorMessage = "An error occurred. Please try again or contact support.";
1088 - }
1089 -
1090 - // Handle session reset action (IP changed, session expired, etc.)
1091 - // Silent reset — keep chat UI intact, just get a new session and retry
1092 - 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');
1096 - 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';
1099 - if (shouldUseStreaming(currentModel)) {
1100 - callMxChatStream(originalMessage, callback, botId);
1101 - } else {
1102 - callMxChat(originalMessage, callback, botId);
1103 - }
1104 - }
1105 - return;
1106 - }
1107 -
1108 - // Format user-friendly error message
1109 - let displayMessage = errorMessage;
1110 - if (mxchatChat.is_admin) {
1111 - displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1112 - }
1113 -
1114 - replaceLastMessage("bot", displayMessage, '', [], botId);
1115 -
1116 - if (callback) {
1117 - callback('');
1118 - }
1119 - return; // Exit early for errors
1120 - }
1121 -
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 - // Handle different response formats
1137 - if (data.text || data.html || data.message) {
1138 -
1139 - // Apply response hooks
1140 - if (data.text && typeof customMxChatFilter === 'function') {
1141 - data.text = customMxChatFilter(data.text, "response");
1142 - }
1143 - if (data.message && typeof customMxChatFilter === 'function') {
1144 - data.message = customMxChatFilter(data.message, "response");
1145 - }
1146 -
1147 - // Display the response
1148 - if (data.text && data.html) {
1149 - replaceLastMessage("bot", data.text, data.html, [], botId);
1150 - } else if (data.text) {
1151 - replaceLastMessage("bot", data.text, '', [], botId);
1152 - } else if (data.html) {
1153 - replaceLastMessage("bot", "", data.html, [], botId);
1154 - } else if (data.message) {
1155 - replaceLastMessage("bot", data.message, '', [], botId);
1156 - }
1157 - }
1158 -
1159 - // Handle other response properties
1160 - if (data.data && data.data.filename) {
1161 - showActivePdf(data.data.filename, botId);
1162 - var instance = MxChatInstances.get(botId);
1163 - instance.activePdfFile = data.data.filename;
1164 - }
1165 -
1166 - if (data.redirect_url) {
1167 - setTimeout(() => {
1168 - window.location.href = data.redirect_url;
1169 - }, 1500);
1170 - }
1171 -
1172 - // Ensure chat input is re-enabled (safety net for edge cases)
1173 - enableChatInput(botId);
1174 -
1175 - if (callback) {
1176 - callback(data.text || data.message || '');
1177 - }
1178 -}
1179 -
1180 -// Enhanced updateChatModeIndicator function for immediate DOM updates
1181 -function updateChatModeIndicator(mode, botId) {
1182 - botId = botId || 'default';
1183 - const indicator = getElementDOM(botId, 'chat-mode-indicator');
1184 - if (indicator) {
1185 - const oldText = indicator.textContent;
1186 -
1187 - if (mode === 'agent') {
1188 - indicator.textContent = 'Live Agent';
1189 - startPolling(botId);
1190 - } else {
1191 - // Everything else is AI mode
1192 - const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1193 - indicator.textContent = customAiText;
1194 - stopPolling(botId);
1195 - }
1196 -
1197 - // Force immediate DOM update and reflow
1198 - if (oldText !== indicator.textContent) {
1199 - // Force a reflow to ensure the change is visible immediately
1200 - indicator.style.display = 'none';
1201 - indicator.offsetHeight; // Trigger reflow
1202 - indicator.style.display = '';
1203 -
1204 - // Double-check after a brief moment to ensure the change stuck
1205 - setTimeout(() => {
1206 - if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
1207 - indicator.textContent = 'Live Agent';
1208 - } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
1209 - const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1210 - indicator.textContent = customAiText;
1211 - }
1212 - }, 50);
1213 - }
1214 - }
1215 -}
1216 -
1217 664 // Function to update message during streaming
1218 -function updateStreamingMessage(content, botId) {
1219 - botId = botId || 'default';
1220 -
665 +function updateStreamingMessage(content) {
1221 666 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1222 667 if (typeof customMxChatFilter === 'function') {
1223 668 content = customMxChatFilter(content, "response");
1224 669 }
670 +
671 + const formattedContent = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(content))));
1225 672
1226 - const formattedContent = linkify(content);
673 + // Find the temporary message
674 + const tempMessage = $('.bot-message.temporary-message').last();
1227 675
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();
1231 -
1232 676 if (tempMessage.length) {
1233 677 // Update existing message
1234 678 tempMessage.html(formattedContent);
1235 679 } else {
1236 680 // Create new temporary message if it doesn't exist
1237 - appendMessage("bot", content, '', [], true, botId);
681 + appendMessage("bot", content, '', [], true);
1238 682 }
1239 683 }
1240 684
685 +// UPGRADE: Function to check if streaming is supported for the current model
1241 686 function isStreamingSupported(model) {
1242 687 if (!model) return false;
1243 688
689 + //console.log("Checking streaming support for model:", model); // Debug log
690 +
691 + // Get the model prefix
1244 692 const modelPrefix = model.split('-')[0].toLowerCase();
693 +
694 + //console.log("Model prefix:", modelPrefix); // Debug log
1245 695
1246 - // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
1247 - const isSupported = modelPrefix === 'gpt' ||
1248 - modelPrefix === 'o1' ||
1249 - modelPrefix === 'claude' ||
1250 - modelPrefix === 'grok' ||
1251 - modelPrefix === 'deepseek' ||
1252 - model === 'openrouter'; // Add this line - check full model name for OpenRouter
696 + // Support streaming for OpenAI, Claude, and Grok models
697 + const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude' || modelPrefix === 'grok';
1253 698
699 + //console.log("Streaming supported:", isSupported); // Debug log
700 +
1254 701 return isSupported;
1255 702 }
1256 703
1257 -// 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);
704 +// Update the event handlers to use the correct function names
705 +$('#send-button').off('click').on('click', function() {
706 + sendMessage(); // Use the updated sendMessage function
1266 707 });
1267 708
1268 -// Override enter key handler (using event delegation)
1269 -$(document).on('keypress', '.chat-input', function(e) {
709 +// Override enter key handler
710 +$('#chat-input').off('keypress').on('keypress', function(e) {
1270 711 if (e.which == 13 && !e.shiftKey) {
1271 712 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);
713 + sendMessage(); // Use the updated sendMessage function
1278 714 }
1279 715 });
1280 716
1281 717
1282 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
718 +function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
1283 719 try {
1284 720 // Determine styles based on sender type
1285 721 let messageClass, bgColor, fontColor;
1286 722
@@ -1301,29 +737,25 @@
1301 737 }
1302 738
1303 739 const messageDiv = $('<div>')
1304 740 .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({
741 + .attr('dir', 'auto')
742 + .css({
1315 743 'background': bgColor,
1316 744 'color': fontColor,
1317 745 'margin-bottom': '1em'
1318 746 });
747 +
748 + // Process the message content based on sender
749 + let fullMessage;
750 + if (sender === "user") {
751 + // For user messages, apply linkify after sanitization
752 + fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
753 + } else {
754 + // For bot/agent messages, preserve HTML
755 + fullMessage = messageText;
1319 756 }
1320 757
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 758 // Add images if provided
1327 759 if (images && images.length > 0) {
1328 760 fullMessage += '<div class="image-gallery" dir="auto">';
1329 761 images.forEach(img => {
@@ -1329,9 +761,9 @@
1329 761 images.forEach(img => {
1330 762 const safeTitle = sanitizeUserInput(img.title);
1331 763 const safeUrl = encodeURI(img.image_url);
1332 764 const safeThumbnail = encodeURI(img.thumbnail_url);
1333 -
765 +
1334 766 fullMessage += `
1335 767 <div style="margin-bottom: 10px;">
1336 768 <strong>${safeTitle}</strong><br>
1337 769 <a href="${safeUrl}" target="_blank">
@@ -1343,14 +775,9 @@
1343 775 }
1344 776
1345 777 // Append HTML content if provided
1346 778 if (messageHtml && sender !== "user") {
1347 - // Only add line breaks if there's actual text content before the HTML
1348 - if (fullMessage && fullMessage.trim()) {
1349 - fullMessage += '<br><br>' + messageHtml;
1350 - } else {
1351 - fullMessage = messageHtml;
1352 - }
779 + fullMessage += '<br><br>' + messageHtml;
1353 780 }
1354 781
1355 782 messageDiv.html(fullMessage);
1356 783
@@ -1357,62 +784,58 @@
1357 784 if (isTemporary) {
1358 785 messageDiv.addClass('temporary-message');
1359 786 }
1360 787
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() {
788 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1364 789 // FIXED: Use event delegation for link tracking
1365 790 if (sender === "bot" || sender === "agent") {
1366 - attachLinkTracking(messageDiv, messageText, botId);
791 + attachLinkTracking(messageDiv, messageText);
1367 792 }
1368 -
793 +
1369 794 if (sender === "bot") {
1370 - const lastUserMessage = $chatBox.find('.user-message').last();
795 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1371 796 if (lastUserMessage.length) {
1372 - scrollElementToTop(lastUserMessage, botId);
797 + scrollElementToTop(lastUserMessage);
1373 798 }
1374 799 }
1375 800 });
1376 801
1377 802 if (messageText.id) {
1378 - var instance = MxChatInstances.get(botId);
1379 - instance.lastSeenMessageId = messageText.id;
1380 - hideNotification(botId);
803 + lastSeenMessageId = messageText.id;
804 + hideNotification();
1381 805 }
1382 806 } catch (error) {
1383 - // Error rendering message - silently continue
807 + console.error("Error rendering message:", error);
1384 808 }
1385 809 }
1386 810
1387 -// Helper function to attach link tracking with proper event handling
1388 -function attachLinkTracking(messageDiv, messageText, botId) {
1389 - botId = botId || 'default';
811 +// NEW: Helper function to attach link tracking with proper event handling
812 +function attachLinkTracking(messageDiv, messageText) {
1390 813 // Use a slight delay to ensure DOM is ready
1391 814 setTimeout(function() {
1392 815 const links = messageDiv.find('a[href]').not('[data-tracked]');
1393 -
816 +
1394 817 links.each(function() {
1395 818 const $link = $(this);
1396 819 const originalHref = $link.attr('href');
1397 -
820 +
1398 821 // Mark as tracked to avoid duplicate handlers
1399 822 $link.attr('data-tracked', 'true');
1400 -
823 +
1401 824 // Only track external URLs
1402 825 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1403 826 // Remove any existing click handlers first
1404 827 $link.off('click.tracking');
1405 -
828 +
1406 829 // Add new click handler with namespace
1407 830 $link.on('click.tracking', function(e) {
1408 831 e.preventDefault();
1409 832 e.stopPropagation();
1410 -
1411 - const messageContext = typeof messageText === 'string'
1412 - ? messageText.substring(0, 200)
833 +
834 + const messageContext = typeof messageText === 'string'
835 + ? messageText.substring(0, 200)
1413 836 : '';
1414 -
837 +
1415 838 // Track the click
1416 839 $.ajax({
1417 840 url: mxchatChat.ajax_url,
1418 841 type: 'POST',
@@ -1417,9 +840,9 @@
1417 840 url: mxchatChat.ajax_url,
1418 841 type: 'POST',
1419 842 data: {
1420 843 action: 'mxchat_track_url_click',
1421 - session_id: getChatSession(botId),
844 + session_id: getChatSession(),
1422 845 url: originalHref,
1423 846 message_context: messageContext,
1424 847 nonce: mxchatChat.nonce
1425 848 },
@@ -1431,9 +854,9 @@
1431 854 window.location.href = originalHref;
1432 855 }
1433 856 }
1434 857 });
1435 -
858 +
1436 859 return false; // Extra insurance to prevent default
1437 860 });
1438 861 }
1439 862 });
@@ -1439,12 +862,11 @@
1439 862 });
1440 863 }, 100); // Small delay to ensure DOM is ready
1441 864 }
1442 865
1443 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
866 +function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1444 867 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();
868 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1447 869
1448 870 // Determine styles
1449 871 let bgColor, fontColor;
1450 872 if (sender === "user") {
@@ -1457,20 +879,11 @@
1457 879 bgColor = botMessageBgColor;
1458 880 fontColor = botMessageFontColor;
1459 881 }
1460 882
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 -
883 + var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
1466 884 if (responseHtml) {
1467 - // Only add line breaks if there's actual text content before the HTML
1468 - if (fullMessage && fullMessage.trim()) {
1469 - fullMessage += '<br><br>' + responseHtml;
1470 - } else {
1471 - fullMessage = responseHtml;
1472 - }
885 + fullMessage += '<br><br>' + responseHtml;
1473 886 }
1474 887
1475 888 if (images.length > 0) {
1476 889 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1486,423 +899,158 @@
1486 899 fullMessage += '</div>';
1487 900 }
1488 901
1489 902 if (lastMessageDiv.length) {
1490 - // Replace content immediately to prevent visual gap between thinking dots and response
1491 - lastMessageDiv
1492 - .html(fullMessage)
1493 - .removeClass('bot-message user-message temporary-message')
1494 - .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({
1501 - 'background-color': bgColor,
1502 - 'color': fontColor,
1503 - });
1504 - }
1505 -
1506 - // Handle link tracking and scroll
1507 - if (sender === "bot" || sender === "agent") {
1508 - attachLinkTracking(lastMessageDiv, responseText, botId);
1509 -
1510 - const lastUserMessage = $chatBox.find('.user-message').last();
1511 - if (lastUserMessage.length) {
1512 - scrollElementToTop(lastUserMessage, botId);
1513 - }
1514 - // Show notification if chat is hidden
1515 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1516 - if ($floatingChatbot.hasClass('hidden')) {
1517 - showNotification(botId);
1518 - }
1519 - }
1520 -
1521 - // Re-enable chat input after response is displayed
1522 - enableChatInput(botId);
903 + lastMessageDiv.fadeOut(200, function() {
904 + $(this)
905 + .html(fullMessage)
906 + .removeClass('bot-message user-message')
907 + .addClass(messageClass)
908 + .attr('dir', 'auto')
909 + .css({
910 + 'background-color': bgColor,
911 + 'color': fontColor,
912 + })
913 + .removeClass('temporary-message')
914 + .fadeIn(200, function() {
915 + // FIXED: Use the helper function for link tracking
916 + if (sender === "bot" || sender === "agent") {
917 + attachLinkTracking($(this), responseText);
918 + }
919 +
920 + if (sender === "bot" || sender === "agent") {
921 + const lastUserMessage = $('#chat-box').find('.user-message').last();
922 + if (lastUserMessage.length) {
923 + scrollElementToTop(lastUserMessage);
924 + }
925 + // Show notification if chat is hidden
926 + if ($('#floating-chatbot').hasClass('hidden')) {
927 + showNotification();
928 + }
929 + }
930 + });
931 + });
1523 932 } else {
1524 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1525 - // Re-enable chat input after response is displayed
1526 - enableChatInput(botId);
933 + appendMessage(sender, responseText, responseHtml, images);
1527 934 }
1528 935 }
1529 936
1530 937
1531 - function appendThinkingMessage(botId) {
1532 - botId = botId || 'default';
938 + function appendThinkingMessage() {
939 + // Remove any existing thinking dots first
940 + $('.thinking-dots').remove();
1533 941
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 942 // Retrieve the bot message font color and background color
1549 943 var botMessageFontColor = mxchatChat.bot_message_font_color;
1550 944 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1551 945
1552 - // Build thinking dots HTML - skip inline colors if AI theme is active
1553 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
946 +
1554 947 var thinkingHtml = '<div class="thinking-dots-container">' +
1555 948 '<div class="thinking-dots">' +
1556 - '<span class="dot"' + dotStyle + '></span>' +
1557 - '<span class="dot"' + dotStyle + '></span>' +
1558 - '<span class="dot"' + dotStyle + '></span>' +
949 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
950 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
951 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1559 952 '</div>' +
1560 953 '</div>';
1561 954
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);
955 + // Append the thinking dots to the chat container (or within the temporary message div)
956 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
957 + scrollToBottom();
1566 958 }
959 +
960 + function removeThinkingDots() {
961 + $('.thinking-dots').closest('.temporary-message').remove();
962 + }
1567 963
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();
1574 - }
1575 964
1576 965 // ====================================
1577 966 // TEXT FORMATTING & PROCESSING
1578 967 // ====================================
1579 968
969 +
1580 970 function linkify(inputText) {
1581 - if (!inputText) {
1582 - return '';
1583 - }
971 + if (!inputText) return '';
972 +
973 + // Process markdown headers
974 + let processedText = formatMarkdownHeaders(inputText);
975 +
976 + // Process markdown links
977 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
978 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
979 + const safeUrl = encodeURI(url);
980 + const safeText = sanitizeUserInput(text);
981 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
982 + });
1584 983
1585 - // Helper function to check if URL is already encoded
1586 - function isUrlEncoded(url) {
1587 - // Check for % followed by exactly 2 hex digits
1588 - return /%[0-9a-fA-F]{2}/.test(url);
1589 - }
984 + // Process phone numbers (tel:)
985 + const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
986 + processedText = processedText.replace(phonePattern, (match, text, phone) => {
987 + const safePhone = encodeURI(phone);
988 + const safeText = sanitizeUserInput(text);
989 + return `<a href="${safePhone}">${safeText}</a>`;
990 + });
1590 991
1591 - // Helper function to safely encode URLs only if needed
1592 - function safeEncodeUrl(url) {
1593 - // If URL already contains encoded characters, return as-is
1594 - if (isUrlEncoded(url)) {
1595 - return url;
1596 - }
1597 - // Otherwise, encode it
1598 - return encodeURI(url);
1599 - }
1600 -
1601 - // Process markdown headers FIRST
1602 - let processedText = formatMarkdownHeaders(inputText);
1603 -
1604 - // Process text styling (bold, italic, strikethrough)
1605 - processedText = formatTextStyling(processedText);
1606 -
1607 - // Process code blocks BEFORE processing links
1608 - processedText = formatCodeBlocks(processedText);
1609 -
1610 - // Process markdown tables BEFORE converting newlines to paragraphs
1611 - processedText = formatMarkdownTables(processedText);
1612 -
1613 - // NOW convert to paragraphs
1614 - processedText = convertNewlinesToBreaks(processedText);
1615 -
1616 - // IMPORTANT: Handle citation-style brackets FIRST [URL]
1617 - // This prevents them from being processed as markdown links
1618 - // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1619 - processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1620 - // Clean the URL of any trailing punctuation
1621 - let cleanUrl = url.replace(/[.,;!?]+$/, '');
1622 - const safeUrl = safeEncodeUrl(cleanUrl);
1623 - // Return as a proper link without the brackets
1624 - return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1625 - });
1626 -
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 - }
1678 - }
1679 - return result;
1680 - })(processedText);
1681 -
1682 - // Process phone numbers: [text](tel:number)
1683 - const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1684 - processedText = processedText.replace(phonePattern, (match, text, phone) => {
1685 - const safePhone = safeEncodeUrl(phone);
1686 - const safeText = sanitizeUserInput(text);
1687 - return `<a href="${safePhone}">${safeText}</a>`;
1688 - });
1689 -
1690 - // Process mailto links: [text](mailto:email)
1691 - const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1692 - processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1693 - const safeMailto = safeEncodeUrl(mailto);
1694 - const safeText = sanitizeUserInput(text);
1695 - return `<a href="${safeMailto}">${safeText}</a>`;
1696 - });
1697 -
1698 - // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1699 - // Updated pattern to be more careful about what it matches
1700 - const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1701 - processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1702 - // Extra check: make sure this isn't already linked
1703 - if (match.includes('href=') || match.includes('</a>')) {
1704 - return match;
1705 - }
992 + // Process standalone URLs
993 + const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
994 + processedText = processedText.replace(urlPattern, (match, prefix, url) => {
995 + const safeUrl = encodeURI(url);
996 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
997 + });
1706 998
1707 - // Clean trailing punctuation
1708 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1709 - const safeUrl = safeEncodeUrl(cleanUrl);
1710 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1711 - });
1712 -
1713 - // Process www. URLs - but NOT if they're already in <a> tags or brackets
1714 - const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
1715 - processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1716 - // Extra check: make sure this isn't already linked
1717 - if (match.includes('href=') || match.includes('</a>')) {
1718 - return match;
1719 - }
999 + // Process www. URLs
1000 + const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
1001 + processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1002 + const safeUrl = encodeURI(`http://${url}`);
1003 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
1004 + });
1720 1005
1721 - // Clean trailing punctuation
1722 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1723 - const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
1724 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1725 - });
1006 + // Add this after your phone pattern
1007 + const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1008 + processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1009 + const safeMailto = encodeURI(mailto);
1010 + const safeText = sanitizeUserInput(text);
1011 + return `<a href="${safeMailto}">${safeText}</a>`;
1012 + });
1726 1013
1727 - return processedText;
1728 -}
1014 + return processedText;
1015 + }
1729 1016
1730 1017 function formatMarkdownHeaders(text) {
1731 1018 // Handle h1 to h6 headers
1732 - return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1019 + return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
1733 1020 const level = hashes.length;
1734 - return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1021 + return `<h${level} class="chat-heading">${content}</h${level}>`;
1735 1022 });
1736 1023 }
1737 1024
1738 -function formatTextStyling(text) {
1739 - // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
1740 - const protectedSegments = [];
1741 - let protectedText = text;
1742 -
1743 - // Step 1a: Protect HTML href="..." attributes
1744 - protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
1745 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1746 - protectedSegments.push(match);
1747 - return placeholder;
1748 - });
1749 -
1750 - // Step 1b: Protect Markdown links [text](url)
1751 - // This is crucial - we need to protect the URLs in markdown format
1752 - protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
1753 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1754 - protectedSegments.push(match);
1755 - return placeholder;
1756 - });
1757 -
1758 - // Step 1c: Also protect bare URLs that might exist
1759 - protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
1760 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1761 - protectedSegments.push(match);
1762 - return placeholder;
1763 - });
1764 -
1765 - // Step 2: Now apply text styling to the protected text
1766 - // Handle bold text (**text**)
1767 - protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1768 -
1769 - // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
1770 - // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
1771 - protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1772 -
1773 - // Handle underscores for italic - Safari-compatible (no lookbehind)
1774 - // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
1775 - protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
1776 -
1777 - // Handle strikethrough (~~text~~)
1778 - protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
1779 -
1780 - // Step 3: Restore all protected segments
1781 - protectedSegments.forEach((original, index) => {
1782 - const placeholder = `__PROTECTED_${index}__`;
1783 - protectedText = protectedText.replace(placeholder, original);
1784 - });
1785 -
1786 - return protectedText;
1787 -}
1788 1025 function formatBoldText(text) {
1789 - // This function is kept for compatibility but now uses formatTextStyling
1790 - return formatTextStyling(text);
1026 + return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1791 1027 }
1792 1028
1793 -function convertNewlinesToBreaks(text) {
1794 - // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1795 - const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1029 + function convertNewlinesToBreaks(text) {
1030 + // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1031 + const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1032 +
1033 + // Wrap each paragraph in <p> tags
1034 + return paragraphs
1035 + .map(para => `<p>${para.trim()}</p>`)
1036 + .join('');
1037 + }
1796 1038
1797 - // Filter out empty paragraphs and wrap each paragraph in <p> tags
1798 - return paragraphs
1799 - .map(para => para.trim())
1800 - .filter(para => para.length > 0) // Remove empty paragraphs
1801 - .map(para => `<p>${para}</p>`)
1802 - .join('');
1803 -}
1804 1039 function formatCodeBlocks(text) {
1805 - // Handle fenced code blocks with language specification (```language)
1806 - text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1807 - const lang = language || 'text';
1808 - const escapedCode = escapeHtml(code.trim());
1809 - return `<div class="mxchat-code-block-container">
1810 - <div class="mxchat-code-header">
1811 - <span class="mxchat-code-language">${lang}</span>
1812 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1813 - </div>
1814 - <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1815 - </div>`;
1040 + // First handle raw PHP tags
1041 + text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1042 + return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
1816 1043 });
1817 -
1818 - // Handle inline code with single backticks
1819 - text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1820 -
1821 - // Handle raw PHP tags (legacy support)
1822 - text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1823 - const escapedCode = escapeHtml(match);
1824 - return `<div class="mxchat-code-block-container">
1825 - <div class="mxchat-code-header">
1826 - <span class="mxchat-code-language">php</span>
1827 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1828 - </div>
1829 - <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1830 - </div>`;
1044 +
1045 + // Then handle code blocks with backticks
1046 + text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
1047 + return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
1831 1048 });
1832 -
1049 +
1833 1050 return text;
1834 1051 }
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 -
1052 +
1905 1053 function sanitizeUserInput(text) {
1906 1054 const div = document.createElement('div');
1907 1055 div.textContent = text;
1908 1056 return div.innerHTML;
@@ -1907,12 +1055,12 @@
1907 1055 div.textContent = text;
1908 1056 return div.innerHTML;
1909 1057 }
1910 1058
1059 +
1911 1060 function escapeHtml(unsafe) {
1912 - // Skip escaping if it's already escaped or contains HTML code block markup
1913 - if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
1914 - unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1061 + // First check if it's already a code block
1062 + if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1915 1063 return unsafe;
1916 1064 }
1917 1065
1918 1066 return unsafe
@@ -1928,25 +1076,15 @@
1928 1076 textArea.innerHTML = text;
1929 1077 return textArea.value;
1930 1078 }
1931 1079
1080 +
1932 1081 // ====================================
1933 1082 // UI & SCROLLING CONTROLS
1934 1083 // ====================================
1935 1084
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');
1085 + function scrollToBottom(instant = false) {
1086 + var chatBox = $('#chat-box');
1949 1087 if (instant) {
1950 1088 // Instantly set the scroll position to the bottom
1951 1089 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1952 1090 } else {
@@ -1955,15 +1093,15 @@
1955 1093 const scrollHeight = chatBox.prop("scrollHeight");
1956 1094 const initialScroll = chatBox.scrollTop();
1957 1095 const distance = scrollHeight - initialScroll;
1958 1096 const duration = 500; // Duration in ms
1959 -
1097 +
1960 1098 function smoothScroll(timestamp) {
1961 1099 if (!start) start = timestamp;
1962 1100 const progress = timestamp - start;
1963 1101 const currentScroll = initialScroll + (distance * (progress / duration));
1964 1102 chatBox.scrollTop(currentScroll);
1965 -
1103 +
1966 1104 if (progress < duration) {
1967 1105 requestAnimationFrame(smoothScroll);
1968 1106 } else {
1969 1107 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -1968,36 +1106,33 @@
1968 1106 } else {
1969 1107 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1970 1108 }
1971 1109 }
1972 -
1110 +
1973 1111 requestAnimationFrame(smoothScroll);
1974 1112 }
1975 1113 }
1976 -
1977 - function scrollElementToTop(element, botId) {
1978 - botId = botId || 'default';
1979 - var chatBox = getElement(botId, 'chat-box');
1114 +
1115 + function scrollElementToTop(element) {
1116 + var chatBox = $('#chat-box');
1980 1117 var elementTop = element.position().top + chatBox.scrollTop();
1981 1118 chatBox.animate({ scrollTop: elementTop }, 500);
1982 1119 }
1983 -
1984 - function showChatWidget(botId) {
1985 - botId = botId || 'default';
1986 - var $button = getElement(botId, 'floating-chatbot-button');
1120 +
1121 + function showChatWidget() {
1987 1122 // First ensure display is set
1988 - $button.css('display', 'flex');
1123 + $('#floating-chatbot-button').css('display', 'flex');
1989 1124 // Then handle the fade
1990 - $button.fadeTo(500, 1);
1125 + $('#floating-chatbot-button').fadeTo(500, 1);
1991 1126 // Force visibility
1992 - $button.removeClass('hidden');
1127 + $('#floating-chatbot-button').removeClass('hidden');
1128 + //console.log('Showing widget');
1993 1129 }
1994 -
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');
1130 +
1131 + function hideChatWidget() {
1132 + $('#floating-chatbot-button').css('display', 'none');
1133 + $('#floating-chatbot-button').addClass('hidden');
1134 + //console.log('Hiding widget');
2000 1135 }
2001 1136
2002 1137 function disableScroll() {
2003 1138 if (isMobile()) {
@@ -2026,15 +1161,18 @@
2026 1161 // NOTIFICATION SYSTEM
2027 1162 // ====================================
2028 1163
2029 1164 function createNotificationBadge() {
1165 + //console.log("Creating notification badge...");
2030 1166 const chatButton = document.getElementById('floating-chatbot-button');
2031 -
1167 + //console.log("Chat button found:", !!chatButton);
1168 +
2032 1169 if (!chatButton) return;
2033 -
1170 +
2034 1171 // Remove any existing badge first
2035 1172 const existingBadge = chatButton.querySelector('.chat-notification-badge');
2036 1173 if (existingBadge) {
1174 + //console.log("Removing existing badge");
2037 1175 existingBadge.remove();
2038 1176 }
2039 1177
2040 1178 notificationBadge = document.createElement('div');
@@ -2056,43 +1194,34 @@
2056 1194 chatButton.appendChild(notificationBadge);
2057 1195
2058 1196 }
2059 1197
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')) {
1198 + function showNotification() {
1199 + const badge = document.getElementById('chat-notification-badge');
1200 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2065 1201 badge.style.display = 'block';
2066 1202 badge.textContent = '1';
2067 1203 }
2068 1204 }
2069 -
2070 - function hideNotification(botId) {
2071 - botId = botId || 'default';
2072 - const badge = getElementDOM(botId, 'chat-notification-badge');
1205 +
1206 + function hideNotification() {
1207 + const badge = document.getElementById('chat-notification-badge');
2073 1208 if (badge) {
2074 1209 badge.style.display = 'none';
2075 1210 }
2076 1211 }
2077 -
2078 - function startNotificationChecking(botId) {
2079 - botId = botId || 'default';
1212 +
1213 + function startNotificationChecking() {
2080 1214 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2081 1215 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
1216 +
1217 + createNotificationBadge();
1218 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2088 1219 }
2089 -
2090 - function stopNotificationChecking(botId) {
2091 - botId = botId || 'default';
2092 - var instance = MxChatInstances.get(botId);
2093 - if (instance.notificationCheckInterval) {
2094 - clearInterval(instance.notificationCheckInterval);
1220 +
1221 + function stopNotificationChecking() {
1222 + if (notificationCheckInterval) {
1223 + clearInterval(notificationCheckInterval);
2095 1224 }
2096 1225 }
2097 1226
2098 1227 function checkForNewMessages() {
@@ -2118,35 +1247,50 @@
2118 1247 });
2119 1248 }
2120 1249
2121 1250
2122 -// ====================================
2123 -// LIVE AGENT FUNCTIONALITY
2124 -// ====================================
2125 -
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 -}
2135 -
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;
1251 + // ====================================
1252 + // LIVE AGENT FUNCTIONALITY
1253 + // ====================================
1254 +
1255 + function updateChatModeIndicator(mode) {
1256 + const indicator = document.getElementById('chat-mode-indicator');
1257 + if (indicator) {
1258 + // For Live Agent, keep as is; for AI mode, use the customized text
1259 + if (mode === 'agent') {
1260 + indicator.textContent = 'Live Agent';
1261 + } else {
1262 + // Get the custom AI agent text from a data attribute we'll add to the element
1263 + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1264 + indicator.textContent = customAiText;
1265 + }
1266 + }
1267 + // Start or stop polling based on mode
1268 + if (mode === 'agent') {
1269 + startPolling();
1270 + } else {
1271 + stopPolling();
1272 + }
2142 1273 }
2143 -}
2144 -
2145 -function checkForAgentMessages(botId) {
2146 - botId = botId || 'default';
2147 - var instance = MxChatInstances.get(botId);
2148 - const sessionId = getChatSession(botId);
1274 +
1275 + function startPolling() {
1276 + // Clear any existing interval first
1277 + stopPolling();
1278 + // Start new polling interval
1279 + pollingInterval = setInterval(checkForAgentMessages, 5000);
1280 + //console.log("Started agent message polling");
1281 + }
1282 +
1283 + function stopPolling() {
1284 + if (pollingInterval) {
1285 + clearInterval(pollingInterval);
1286 + pollingInterval = null;
1287 + //console.log("Stopped agent message polling");
1288 + }
1289 + }
1290 +
1291 +function checkForAgentMessages() {
1292 + const sessionId = getChatSession();
2149 1293 $.ajax({
2150 1294 url: mxchatChat.ajax_url,
2151 1295 type: 'POST',
2152 1296 dataType: 'json',
@@ -2152,44 +1296,35 @@
2152 1296 dataType: 'json',
2153 1297 data: {
2154 1298 action: 'mxchat_fetch_new_messages',
2155 1299 session_id: sessionId,
2156 - last_seen_id: instance.lastSeenMessageId,
2157 - persistence_enabled: 'true',
1300 + last_seen_id: lastSeenMessageId,
1301 + persistence_enabled: 'true', // Add this too
2158 1302 nonce: mxchatChat.nonce
2159 1303 },
2160 1304 success: function (response) {
2161 1305 if (response.success && response.data?.new_messages) {
2162 1306 let hasNewMessage = false;
2163 -
1307 +
2164 1308 response.data.new_messages.forEach(function (message) {
2165 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1309 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2166 1310 hasNewMessage = true;
2167 - appendMessage("agent", message.content, '', [], false, botId);
2168 - instance.lastSeenMessageId = message.id;
2169 - instance.processedMessageIds.add(message.id);
1311 + // CHANGE THIS LINE:
1312 + appendMessage("agent", message.content); // Instead of replaceLastMessage
1313 + lastSeenMessageId = message.id;
1314 + processedMessageIds.add(message.id);
2170 1315 }
2171 1316 });
2172 1317
2173 - if (hasNewMessage) {
2174 - enableChatInput(botId);
1318 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1319 + showNotification();
2175 1320 }
2176 -
2177 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2178 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2179 - showNotification(botId);
2180 - }
2181 -
2182 - scrollToBottom(botId, true);
1321 +
1322 + scrollToBottom(true);
2183 1323 }
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 1324 },
2190 1325 error: function (xhr, status, error) {
2191 - // Polling error - silently continue
1326 + //console.error("Polling error:", xhr, status, error);
2192 1327 }
2193 1328 });
2194 1329 }
2195 1330
@@ -2195,166 +1330,120 @@
2195 1330
2196 1331 // ====================================
2197 1332 // CHAT HISTORY & PERSISTENCE
2198 1333 // ====================================
2199 -
2200 -function loadChatHistory(botId, onComplete) {
2201 - botId = botId || 'default';
2202 - var instance = MxChatInstances.get(botId);
2203 -
2204 - // Prevent duplicate loading
2205 - if (instance.chatHistoryLoaded) {
2206 - if (onComplete) onComplete();
2207 - return;
2208 - }
2209 -
2210 - // Use getChatSession which returns null if no session exists (does NOT create one)
2211 - var sessionId = getChatSession(botId);
2212 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2213 -
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 - if (chatPersistenceEnabled && sessionId) {
2222 - $.ajax({
2223 - url: mxchatChat.ajax_url,
2224 - type: 'POST',
2225 - dataType: 'json',
2226 - data: {
2227 - action: 'mxchat_fetch_conversation_history',
2228 - session_id: sessionId
2229 - },
2230 - success: function(response) {
2231 - // Handle session reset (IP changed while user was away)
2232 - 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();
2237 - return;
2238 - }
2239 -
2240 - // Check if the response indicates success
2241 - if (response.success) {
2242 - // Handle case where conversation data exists and is an array
2243 - if (response.data && Array.isArray(response.data.conversation)) {
2244 - var $chatBox = getElement(botId, 'chat-box');
1334 +
1335 + function loadChatHistory() {
1336 + var sessionId = getChatSession();
1337 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1338 +
1339 + if (chatPersistenceEnabled && sessionId) {
1340 + $.ajax({
1341 + url: mxchatChat.ajax_url,
1342 + type: 'POST',
1343 + dataType: 'json',
1344 + data: {
1345 + action: 'mxchat_fetch_conversation_history',
1346 + session_id: sessionId
1347 + },
1348 + success: function(response) {
1349 + if (response.success && response.data && Array.isArray(response.data.conversation)) {
1350 +
1351 +
1352 + var $chatBox = $('#chat-box');
2245 1353 var $fragment = $(document.createDocumentFragment());
2246 - let highestMessageId = instance.lastSeenMessageId;
2247 -
2248 - // Update chat mode if provided
1354 + let highestMessageId = lastSeenMessageId;
1355 +
2249 1356 if (response.data.chat_mode) {
2250 - updateChatModeIndicator(response.data.chat_mode, botId);
1357 + updateChatModeIndicator(response.data.chat_mode);
2251 1358 }
2252 -
2253 - // Only process if there are actual messages
2254 - if (response.data.conversation.length > 0) {
2255 - // IMPORTANT: Clear existing messages before loading history
2256 - $chatBox.empty();
2257 -
2258 - $.each(response.data.conversation, function(index, message) {
2259 - // Skip agent messages if persistence is off
2260 - if (!chatPersistenceEnabled && message.role === 'agent') {
2261 - return;
2262 - }
2263 -
2264 - var messageClass, messageBgColor, messageFontColor;
2265 -
2266 - switch (message.role) {
2267 - case 'user':
2268 - messageClass = 'user-message';
2269 - messageBgColor = userMessageBgColor;
2270 - messageFontColor = userMessageFontColor;
2271 - break;
2272 - case 'agent':
2273 - messageClass = 'agent-message';
2274 - messageBgColor = liveAgentMessageBgColor;
2275 - messageFontColor = liveAgentMessageFontColor;
2276 - break;
2277 - default:
2278 - messageClass = 'bot-message';
2279 - messageBgColor = botMessageBgColor;
2280 - messageFontColor = botMessageFontColor;
2281 - break;
2282 - }
2283 -
2284 - var messageElement = $('<div>').addClass(messageClass)
2285 - .css({
2286 - 'background': messageBgColor,
2287 - 'color': messageFontColor
2288 - });
2289 -
2290 - var content = message.content;
2291 - content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2292 - content = decodeHTMLEntities(content);
2293 -
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")) {
2305 - messageElement.html(content);
2306 - } else {
2307 - var formattedContent = linkify(content);
2308 - messageElement.html(formattedContent);
2309 - }
2310 -
2311 - $fragment.append(messageElement);
2312 -
2313 - // Track message IDs
1359 +
1360 + $.each(response.data.conversation, function(index, message) {
1361 + // Skip agent messages if persistence is off
1362 + if (!chatPersistenceEnabled && message.role === 'agent') {
1363 + return;
1364 + }
1365 +
1366 + var messageClass, messageBgColor, messageFontColor;
1367 +
1368 + switch (message.role) {
1369 + case 'user':
1370 + messageClass = 'user-message';
1371 + messageBgColor = userMessageBgColor;
1372 + messageFontColor = userMessageFontColor;
1373 + break;
1374 + case 'agent':
1375 + messageClass = 'agent-message';
1376 + messageBgColor = liveAgentMessageBgColor;
1377 + messageFontColor = liveAgentMessageFontColor;
1378 + break;
1379 + default:
1380 + messageClass = 'bot-message';
1381 + messageBgColor = botMessageBgColor;
1382 + messageFontColor = botMessageFontColor;
1383 + break;
1384 + }
1385 +
1386 + var messageElement = $('<div>').addClass(messageClass)
1387 + .css({
1388 + 'background': messageBgColor,
1389 + 'color': messageFontColor
1390 + });
1391 +
1392 + var content = message.content;
1393 + content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1394 + content = decodeHTMLEntities(content);
1395 +
1396 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1397 + messageElement.html(content);
1398 + } else {
1399 + var formattedContent = linkify(
1400 + formatBoldText(
1401 + convertNewlinesToBreaks(formatCodeBlocks(content))
1402 + )
1403 + );
1404 + messageElement.html(formattedContent);
1405 + }
1406 +
1407 + $fragment.append(messageElement);
1408 +
1409 + // In loadChatHistory, change this part:
2314 1410 if (message.id) {
2315 1411 highestMessageId = Math.max(highestMessageId, message.id);
2316 - instance.processedMessageIds.add(message.id);
1412 + processedMessageIds.add(message.id); // Add all message IDs to processed set
2317 1413 }
2318 - });
2319 -
2320 - // Only append messages and scroll if we have content
2321 - $chatBox.append($fragment);
2322 - scrollToBottom(botId, true);
2323 -
2324 - // 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);
1414 + });
1415 +
1416 + $chatBox.append($fragment);
1417 + scrollToBottom(true);
1418 +
1419 + if (response.data.conversation.length > 0 && hasQuickQuestions()) {
1420 + collapseQuickQuestions();
1421 + }
1422 +
1423 + // Update lastSeenMessageId after history loads
1424 + lastSeenMessageId = highestMessageId;
1425 +
1426 + // Only update chat mode if persistence is enabled
1427 + if (chatPersistenceEnabled && response.data.conversation.length > 0) {
1428 + var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1429 + if (lastMessage.role === 'agent') {
1430 + updateChatModeIndicator('agent');
2328 1431 }
2329 -
2330 - // Update lastSeenMessageId after history loads
2331 - instance.lastSeenMessageId = highestMessageId;
2332 -
2333 - // Only update chat mode if persistence is enabled and we have messages
2334 - if (chatPersistenceEnabled) {
2335 - var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2336 - if (lastMessage.role === 'agent') {
2337 - updateChatModeIndicator('agent', botId);
2338 - }
2339 - }
2340 -
2341 - // Mark as loaded ONLY after successful load
2342 - instance.chatHistoryLoaded = true;
2343 1432 }
1433 + } else {
1434 + console.warn("No conversation history found.");
2344 1435 }
1436 + },
1437 + error: function(xhr, status, error) {
1438 + //console.error("Error loading chat history:", status, error);
1439 + appendMessage("bot", "Unable to load chat history.");
2345 1440 }
2346 - if (onComplete) onComplete();
2347 - },
2348 - error: function(xhr, status, error) {
2349 - // Error loading chat history - silently continue
2350 - if (onComplete) onComplete();
2351 - }
2352 - });
2353 - } else {
2354 - if (onComplete) onComplete();
1441 + });
1442 + } else {
1443 + console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
1444 + }
2355 1445 }
2356 -}
2357 1446
2358 1447
2359 1448 // ====================================
2360 1449 // FILE UPLOAD FUNCTIONALITY
@@ -2366,42 +1455,40 @@
2366 1455 element.addEventListener(eventType, handler);
2367 1456 }
2368 1457 }
2369 1458
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');
2374 -
1459 + function showActivePdf(filename) {
1460 + const container = document.getElementById('active-pdf-container');
1461 + const nameElement = document.getElementById('active-pdf-name');
1462 +
2375 1463 if (!container || !nameElement) {
1464 + //console.error('PDF container elements not found');
2376 1465 return;
2377 1466 }
2378 -
1467 +
2379 1468 nameElement.textContent = filename;
2380 1469 container.style.display = 'flex';
2381 1470 }
2382 -
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');
2387 -
1471 +
1472 + function showActiveWord(filename) {
1473 + const container = document.getElementById('active-word-container');
1474 + const nameElement = document.getElementById('active-word-name');
1475 +
2388 1476 if (!container || !nameElement) {
1477 + //console.error('Word document container elements not found');
2389 1478 return;
2390 1479 }
2391 -
1480 +
2392 1481 nameElement.textContent = filename;
2393 1482 container.style.display = 'flex';
2394 1483 }
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 -
1484 +
1485 + function removeActivePdf() {
1486 + const container = document.getElementById('active-pdf-container');
1487 + const nameElement = document.getElementById('active-pdf-name');
1488 +
1489 + if (!container || !nameElement || !activePdfFile) return;
1490 +
2404 1491 fetch(mxchatChat.ajax_url, {
2405 1492 method: 'POST',
2406 1493 headers: {
2407 1494 'Content-Type': 'application/x-www-form-urlencoded',
@@ -2407,9 +1494,9 @@
2407 1494 'Content-Type': 'application/x-www-form-urlencoded',
2408 1495 },
2409 1496 body: new URLSearchParams({
2410 1497 'action': 'mxchat_remove_pdf',
2411 - 'session_id': getChatSession(botId),
1498 + 'session_id': sessionId,
2412 1499 'nonce': mxchatChat.nonce
2413 1500 })
2414 1501 })
2415 1502 .then(response => response.json())
@@ -2421,12 +1508,12 @@
2421 1508 appendMessage('bot', 'PDF removed.');
2422 1509 }
2423 1510 })
2424 1511 .catch(error => {
2425 - // Error removing PDF - silently continue
1512 + //console.error('Error removing PDF:', error);
2426 1513 });
2427 1514 }
2428 -
1515 +
2429 1516 function removeActiveWord() {
2430 1517 const container = document.getElementById('active-word-container');
2431 1518 const nameElement = document.getElementById('active-word-name');
2432 1519
@@ -2452,53 +1539,56 @@
2452 1539 appendMessage('bot', 'Word document removed.');
2453 1540 }
2454 1541 })
2455 1542 .catch(error => {
2456 - // Error removing Word document - silently continue
1543 + //console.error('Error removing Word document:', error);
2457 1544 });
2458 1545 }
2459 -
1546 +
2460 1547 // ====================================
2461 1548 // CONSENT & COMPLIANCE (GDPR)
2462 1549 // ====================================
2463 -
2464 - function initializeChatVisibility(botId) {
2465 - botId = botId || 'default';
2466 - const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2467 - mxchatChat.complianz_toggle === '1' ||
1550 +
1551 + function initializeChatVisibility() {
1552 + //console.log('Initializing chat visibility');
1553 + const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1554 + mxchatChat.complianz_toggle === '1' ||
2468 1555 mxchatChat.complianz_toggle === 1;
2469 -
1556 +
2470 1557 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2471 1558 // Initial check
2472 - checkConsentAndShowChat(botId);
2473 -
1559 + checkConsentAndShowChat();
1560 +
2474 1561 // Listen for consent changes
2475 1562 $(document).on('cmplz_status_change', function(event) {
2476 - checkConsentAndShowChat(botId);
1563 + //console.log('Status change detected');
1564 + checkConsentAndShowChat();
2477 1565 });
2478 1566 } else {
2479 1567 // If Complianz is not enabled, always show
2480 - getElement(botId, 'floating-chatbot-button')
1568 + $('#floating-chatbot-button')
2481 1569 .css('display', 'flex')
2482 1570 .removeClass('hidden no-consent')
2483 1571 .fadeTo(500, 1);
2484 -
1572 +
2485 1573 // Also check pre-chat message when Complianz is not enabled
2486 - checkPreChatDismissal(botId);
1574 + checkPreChatDismissal();
2487 1575 }
2488 1576 }
2489 1577
2490 -
2491 - function checkConsentAndShowChat(botId) {
2492 - botId = botId || 'default';
1578 +
1579 + function checkConsentAndShowChat() {
2493 1580 var consentStatus = cmplz_has_consent('marketing');
2494 1581 var consentType = complianz.consenttype;
2495 -
2496 - let $widget = getElement(botId, 'floating-chatbot-button');
2497 - let $chatbot = getElement(botId, 'floating-chatbot');
2498 - let $preChat = getElement(botId, 'pre-chat-message');
2499 -
1582 +
1583 + //console.log('Checking consent:', {status: consentStatus,type: consentType});
1584 +
1585 + let $widget = $('#floating-chatbot-button');
1586 + let $chatbot = $('#floating-chatbot');
1587 + let $preChat = $('#pre-chat-message');
1588 +
2500 1589 if (consentStatus === true) {
1590 + //console.log('Consent granted - showing widget');
2501 1591 $widget
2502 1592 .removeClass('no-consent')
2503 1593 .css('display', 'flex')
2504 1594 .removeClass('hidden')
@@ -2503,12 +1593,13 @@
2503 1593 .css('display', 'flex')
2504 1594 .removeClass('hidden')
2505 1595 .fadeTo(500, 1);
2506 1596 $chatbot.removeClass('no-consent');
2507 -
1597 +
2508 1598 // Show pre-chat message if not dismissed
2509 - checkPreChatDismissal(botId);
1599 + checkPreChatDismissal();
2510 1600 } else {
1601 + //console.log('No consent - hiding widget');
2511 1602 $widget
2512 1603 .addClass('no-consent')
2513 1604 .fadeTo(500, 0, function() {
2514 1605 $(this)
@@ -2515,9 +1606,9 @@
2515 1606 .css('display', 'none')
2516 1607 .addClass('hidden');
2517 1608 });
2518 1609 $chatbot.addClass('no-consent');
2519 -
1610 +
2520 1611 // Hide pre-chat message when no consent
2521 1612 $preChat.hide();
2522 1613 }
2523 1614 }
@@ -2525,38 +1616,46 @@
2525 1616
2526 1617 // ====================================
2527 1618 // PRE-CHAT MESSAGE HANDLING
2528 1619 // ====================================
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;
1620 +
1621 + function checkPreChatDismissal() {
1622 + $.ajax({
1623 + url: mxchatChat.ajax_url,
1624 + type: 'POST',
1625 + data: {
1626 + action: 'mxchat_check_pre_chat_message_status',
1627 + _ajax_nonce: mxchatChat.nonce
1628 + },
1629 + success: function(response) {
1630 + if (response.success && !response.data.dismissed) {
1631 + $('#pre-chat-message').fadeIn(250);
1632 + } else {
1633 + $('#pre-chat-message').hide();
2540 1634 }
2541 - // Expired — clear and show again
2542 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
1635 + },
1636 + error: function() {
1637 + //console.error('Failed to check pre-chat message dismissal status.');
2543 1638 }
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 - }
1639 + });
2549 1640 }
2550 -
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 - }
1641 +
1642 + function handlePreChatDismissal() {
1643 + $('#pre-chat-message').fadeOut(200);
1644 + $.ajax({
1645 + url: mxchatChat.ajax_url,
1646 + type: 'POST',
1647 + data: {
1648 + action: 'mxchat_dismiss_pre_chat_message',
1649 + _ajax_nonce: mxchatChat.nonce
1650 + },
1651 + success: function() {
1652 + $('#pre-chat-message').hide();
1653 + },
1654 + error: function() {
1655 + //console.error('Failed to dismiss pre-chat message.');
1656 + }
1657 + });
2559 1658 }
2560 1659
2561 1660
2562 1661 // ====================================
@@ -2582,104 +1681,78 @@
2582 1681 // ====================================
2583 1682
2584 1683 $(document).on('click', '.mxchat-popular-question', function () {
2585 1684 var question = $(this).text();
2586 - var botId = getBotIdFromElement(this);
2587 -
1685 +
2588 1686 // Append the question as if the user typed it
2589 - appendMessage("user", question, '', [], false, botId);
2590 -
1687 + appendMessage("user", question);
1688 +
2591 1689 // Only collapse if there are questions
2592 - if (hasQuickQuestions(botId)) {
2593 - collapseQuickQuestions(botId);
1690 + if (hasQuickQuestions()) {
1691 + collapseQuickQuestions();
2594 1692 }
2595 -
1693 +
2596 1694 // Send the question to the server
2597 - sendMessageToChatbot(question, botId);
1695 + sendMessageToChatbot(question);
2598 1696 });
2599 1697
2600 1698 $(document).on('click', '.questions-toggle-btn', function(e) {
2601 1699 e.preventDefault();
2602 1700 e.stopPropagation();
2603 - var botId = getBotIdFromElement(this);
2604 - expandQuickQuestions(botId);
1701 + expandQuickQuestions();
2605 1702 });
2606 1703
2607 1704 $(document).on('click', '.questions-collapse-btn', function(e) {
2608 1705 e.preventDefault();
2609 1706 e.stopPropagation();
2610 - var botId = getBotIdFromElement(this);
2611 - collapseQuickQuestions(botId);
1707 + collapseQuickQuestions();
2612 1708 });
2613 1709
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');
1710 + // Chatbot visibility toggle handlers
1711 + $(document).on('click', '#floating-chatbot-button', function() {
1712 + var chatbot = $('#floating-chatbot');
1713 + if (chatbot.hasClass('hidden')) {
1714 + chatbot.removeClass('hidden').addClass('visible');
2623 1715 $(this).addClass('hidden');
2624 - $badge.hide(); // Hide notification when opening chat
1716 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
2625 1717 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 - }
1718 + $('#pre-chat-message').fadeOut(250);
2645 1719 } else {
2646 - $chatbot.removeClass('visible').addClass('hidden');
1720 + chatbot.removeClass('visible').addClass('hidden');
2647 1721 $(this).removeClass('hidden');
2648 1722 enableScroll();
2649 - checkPreChatDismissal(botId);
1723 + checkPreChatDismissal();
2650 1724 }
2651 1725 });
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');
1726 +
1727 + $(document).on('click', '#exit-chat-button', function() {
1728 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
1729 + $('#floating-chatbot-button').removeClass('hidden');
2658 1730 enableScroll();
2659 1731 });
2660 -
1732 +
2661 1733 $(document).on('click', '.close-pre-chat-message', function(e) {
2662 1734 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2663 - var botId = getBotIdFromElement(this);
2664 - handlePreChatDismissal(botId);
1735 + $('#pre-chat-message').fadeOut(200, function() {
1736 + $(this).remove();
1737 + });
2665 1738 });
1739 +
2666 1740
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 - });
1741 + // PDF upload button handlers
1742 + if (document.getElementById('pdf-upload-btn')) {
1743 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1744 + document.getElementById('pdf-upload').click();
1745 + });
1746 + }
2681 1747
1748 + // Word upload button handlers
1749 + if (document.getElementById('word-upload-btn')) {
1750 + document.getElementById('word-upload-btn').addEventListener('click', function() {
1751 + document.getElementById('word-upload').click();
1752 + });
1753 + }
1754 +
2682 1755 // PDF file input change handler
2683 1756 addSafeEventListener('pdf-upload', 'change', async function(e) {
2684 1757 const file = e.target.files[0];
2685 1758
@@ -2688,13 +1761,15 @@
2688 1761 return;
2689 1762 }
2690 1763
2691 1764 if (!sessionId) {
1765 + //console.error('No session ID found');
2692 1766 alert('Error: No session ID found');
2693 1767 return;
2694 1768 }
2695 -
1769 +
2696 1770 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1771 + //console.error('mxchatChat not properly configured:', mxchatChat);
2697 1772 alert('Error: Ajax configuration missing');
2698 1773 return;
2699 1774 }
2700 1775
@@ -2736,11 +1811,13 @@
2736 1811 appendMessage('bot', data.data.message);
2737 1812 scrollToBottom();
2738 1813 activePdfFile = data.data.filename;
2739 1814 } else {
1815 + //console.error('Upload failed:', data.data);
2740 1816 alert('Failed to upload PDF. Please try again.');
2741 1817 }
2742 1818 } catch (error) {
1819 + //console.error('Upload error:', error);
2743 1820 alert('Error uploading file. Please try again.');
2744 1821 } finally {
2745 1822 uploadBtn.disabled = false;
2746 1823 sendBtn.disabled = false;
@@ -2758,12 +1835,13 @@
2758 1835 return;
2759 1836 }
2760 1837
2761 1838 if (!sessionId) {
1839 + //console.error('No session ID found');
2762 1840 alert('Error: No session ID found');
2763 1841 return;
2764 1842 }
2765 -
1843 +
2766 1844 // Disable buttons and show loading state
2767 1845 const uploadBtn = document.getElementById('word-upload-btn');
2768 1846 const sendBtn = document.getElementById('send-button');
2769 1847 const originalBtnContent = uploadBtn.innerHTML;
@@ -2801,11 +1879,13 @@
2801 1879 appendMessage('bot', data.data.message);
2802 1880 scrollToBottom();
2803 1881 activeWordFile = data.data.filename;
2804 1882 } else {
1883 + //console.error('Upload failed:', data.data);
2805 1884 alert('Failed to upload Word document. Please try again.');
2806 1885 }
2807 1886 } catch (error) {
1887 + //console.error('Upload error:', error);
2808 1888 alert('Error uploading file. Please try again.');
2809 1889 } finally {
2810 1890 uploadBtn.disabled = false;
2811 1891 sendBtn.disabled = false;
@@ -2863,242 +1943,28 @@
2863 1943 });
2864 1944
2865 1945
2866 1946 // ====================================
2867 -// INIT LOADER & CHAT CONTAINER HELPERS
1947 +// IMPROVED EMAIL COLLECTION SETUP
2868 1948 // ====================================
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 1949
2872 -function showInitLoader(botId) {
2873 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2874 - if (loader) loader.style.display = 'flex';
2875 -}
1950 +// Email collection form setup and handlers
1951 +const emailForm = document.getElementById('email-collection-form');
1952 +const emailBlocker = document.getElementById('email-blocker');
1953 +const chatbotWrapper = document.getElementById('chat-container');
2876 1954
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 -// Only run email collection setup if it's enabled
2921 -if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2922 -
2923 - // Track submitting state per bot
2924 - const emailSubmittingState = {};
2925 -
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); }
2934 - }
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 - }
2954 -
2955 - function isValidEmailAddress(email) {
2956 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2957 - return emailRegex.test(email.trim()) && email.length <= 254;
2958 - }
2959 -
2960 - function isValidNameInput(name) {
2961 - return name && name.trim().length >= 2 && name.trim().length <= 100;
2962 - }
2963 -
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();
2992 - }
2993 -
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}
3019 - `;
3020 - submitButton.style.opacity = '0.8';
3021 - }
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';
3034 - }
3035 - }
3036 - }
3037 -
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) {
3073 - const existingErrors = emailForm.querySelectorAll('.email-error');
3074 - existingErrors.forEach(error => error.remove());
3075 - }
3076 - }
3077 -
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 - }
3090 -
3091 - function checkSessionAndEmailForBot(botId) {
3092 - const sessionId = MxChatInstances.ensureSession(botId);
3093 -
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);
3100 -
1955 +if (emailForm && emailBlocker && chatbotWrapper) {
1956 + // Add loading state management
1957 + let isSubmitting = false;
1958 +
1959 + // Check if email exists for the current session
1960 + function checkSessionAndEmail() {
1961 + const sessionId = getChatSession();
1962 +
1963 + // Add timeout to prevent hanging
1964 + const controller = new AbortController();
1965 + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
1966 +
3101 1967 fetch(mxchatChat.ajax_url, {
3102 1968 method: 'POST',
3103 1969 headers: {
3104 1970 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3106,11 +1972,13 @@
3106 1972 body: new URLSearchParams({
3107 1973 action: 'mxchat_check_email_provided',
3108 1974 session_id: sessionId,
3109 1975 nonce: mxchatChat.nonce,
3110 - })
1976 + }),
1977 + signal: controller.signal
3111 1978 })
3112 1979 .then((response) => {
1980 + clearTimeout(timeoutId);
3113 1981 if (!response.ok) {
3114 1982 throw new Error(`HTTP error! status: ${response.status}`);
3115 1983 }
3116 1984 return response.json();
@@ -3117,79 +1985,157 @@
3117 1985 })
3118 1986 .then((data) => {
3119 1987 if (data.success) {
3120 1988 if (data.data.logged_in || data.data.email) {
3121 - showChatContainerForBot(botId);
1989 + showChatContainer();
3122 1990 } else {
3123 - showEmailFormForBot(botId);
1991 + showEmailForm();
3124 1992 }
3125 1993 } else {
3126 - showEmailFormForBot(botId);
1994 + // On error, default to showing email form
1995 + showEmailForm();
3127 1996 }
3128 1997 })
3129 1998 .catch((error) => {
3130 - showEmailFormForBot(botId);
1999 + clearTimeout(timeoutId);
2000 + console.warn('Email check failed, defaulting to email form:', error);
2001 + showEmailForm();
3131 2002 });
3132 2003 }
3133 2004
3134 - // Event delegation for email form submission
3135 - $(document).on('submit', '.email-collection-form', function(e) {
3136 - e.preventDefault();
3137 - e.stopPropagation();
2005 + // Optimized UI transition functions
2006 + function showEmailForm() {
2007 + emailBlocker.style.display = 'flex';
2008 + chatbotWrapper.style.display = 'none';
2009 + }
3138 2010
3139 - var botId = getBotIdFromElement(this);
2011 + function showChatContainer() {
2012 + // Show chat immediately without delay
2013 + emailBlocker.style.display = 'none';
2014 + chatbotWrapper.style.display = 'flex';
2015 +
2016 + // Load chat history only after showing chat container
2017 + if (typeof loadChatHistory === 'function') {
2018 + loadChatHistory();
2019 + }
2020 + }
3140 2021
2022 + // Enhanced email validation
2023 + function isValidEmail(email) {
2024 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2025 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2026 + }
2027 +
2028 + // Show loading state with spinner
2029 + function setSubmissionState(loading) {
2030 + const submitButton = document.getElementById('email-submit-button');
2031 + const emailInput = document.getElementById('user-email');
2032 +
2033 + if (loading) {
2034 + isSubmitting = true;
2035 + submitButton.disabled = true;
2036 + emailInput.disabled = true;
2037 +
2038 + // Store original content and add spinner
2039 + if (!submitButton.getAttribute('data-original-html')) {
2040 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2041 + }
2042 +
2043 + // Add loading spinner while keeping original text
2044 + const originalText = submitButton.textContent;
2045 + submitButton.innerHTML = `
2046 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2047 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2048 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2049 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2050 + </circle>
2051 + </svg>
2052 + ${originalText}
2053 + `;
2054 +
2055 + submitButton.style.opacity = '0.8';
2056 + } else {
2057 + isSubmitting = false;
2058 + submitButton.disabled = false;
2059 + emailInput.disabled = false;
2060 +
2061 + // Restore original content
2062 + const originalHtml = submitButton.getAttribute('data-original-html');
2063 + if (originalHtml) {
2064 + submitButton.innerHTML = originalHtml;
2065 + }
2066 +
2067 + submitButton.style.opacity = '1';
2068 + }
2069 + }
2070 +
2071 + // Add CSS for spinner animation if not already present
2072 + if (!document.getElementById('email-spinner-styles')) {
2073 + const style = document.createElement('style');
2074 + style.id = 'email-spinner-styles';
2075 + style.textContent = `
2076 + @keyframes spin {
2077 + from { transform: rotate(0deg); }
2078 + to { transform: rotate(360deg); }
2079 + }
2080 + .email-spinner {
2081 + display: inline-block;
2082 + vertical-align: middle;
2083 + }
2084 + `;
2085 + document.head.appendChild(style);
2086 + }
2087 +
2088 + // Handle email form submission with improved error handling
2089 + emailForm.addEventListener('submit', function (event) {
2090 + event.preventDefault();
2091 +
3141 2092 // Prevent double submission
3142 - if (emailSubmittingState[botId]) {
3143 - return false;
2093 + if (isSubmitting) {
2094 + return;
3144 2095 }
3145 2096
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);
2097 + const userEmail = document.getElementById('user-email').value.trim();
2098 + const sessionId = getChatSession();
3151 2099
3152 - // Validate email
2100 + // Validate email before submission
3153 2101 if (!userEmail) {
3154 - showEmailError(botId, 'Please enter your email address.');
3155 - return false;
2102 + showEmailError('Please enter your email address.');
2103 + return;
3156 2104 }
3157 2105
3158 - if (!isValidEmailAddress(userEmail)) {
3159 - showEmailError(botId, 'Please enter a valid email address.');
3160 - return false;
2106 + if (!isValidEmail(userEmail)) {
2107 + showEmailError('Please enter a valid email address.');
2108 + return;
3161 2109 }
3162 2110
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 - }
2111 + // Clear any existing errors
2112 + clearEmailError();
2113 + setSubmissionState(true);
3168 2114
3169 - clearEmailError(botId);
3170 - setEmailSubmissionState(botId, true);
2115 + // Add timeout for submission
2116 + const controller = new AbortController();
2117 + const timeoutId = setTimeout(() => {
2118 + controller.abort();
2119 + setSubmissionState(false);
2120 + showEmailError('Request timed out. Please try again.');
2121 + }, 15000); // 15 second timeout
3171 2122
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 - });
3179 -
3180 - if (userName) {
3181 - formData.append('name', userName);
3182 - }
3183 -
3184 2123 fetch(mxchatChat.ajax_url, {
3185 2124 method: 'POST',
3186 2125 headers: {
3187 2126 'Content-Type': 'application/x-www-form-urlencoded',
3188 2127 },
3189 - body: formData
2128 + body: new URLSearchParams({
2129 + action: 'mxchat_handle_save_email_and_response',
2130 + email: userEmail,
2131 + session_id: sessionId,
2132 + nonce: mxchatChat.nonce,
2133 + }),
2134 + signal: controller.signal
3190 2135 })
3191 2136 .then((response) => {
2137 + clearTimeout(timeoutId);
3192 2138 if (!response.ok) {
3193 2139 throw new Error(`HTTP error! status: ${response.status}`);
3194 2140 }
3195 2141 return response.json();
@@ -3194,166 +2140,185 @@
3194 2140 }
3195 2141 return response.json();
3196 2142 })
3197 2143 .then((data) => {
3198 - setEmailSubmissionState(botId, false);
3199 -
2144 + setSubmissionState(false);
2145 +
3200 2146 if (data.success) {
3201 - showChatContainerForBot(botId);
2147 + // Show chat immediately
2148 + showChatContainer();
3202 2149
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, '');
3209 - }
3210 -
2150 + // Handle bot response if provided
3211 2151 if (data.message && typeof appendMessage === 'function') {
3212 2152 setTimeout(() => {
3213 - appendMessage('bot', data.message, '', [], false, botId);
2153 + appendMessage('bot', data.message);
3214 2154 if (typeof scrollToBottom === 'function') {
3215 - scrollToBottom(botId);
2155 + scrollToBottom();
3216 2156 }
3217 2157 }, 100);
3218 2158 }
3219 2159 } else {
3220 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2160 + showEmailError(data.message || 'Failed to save email. Please try again.');
3221 2161 }
3222 2162 })
3223 2163 .catch((error) => {
3224 - setEmailSubmissionState(botId, false);
3225 - showEmailError(botId, 'An error occurred. Please try again.');
2164 + clearTimeout(timeoutId);
2165 + setSubmissionState(false);
2166 +
2167 + if (error.name === 'AbortError') {
2168 + showEmailError('Request timed out. Please try again.');
2169 + } else {
2170 + console.error('Email submission error:', error);
2171 + showEmailError('An error occurred. Please try again.');
2172 + }
3226 2173 });
3227 -
3228 - return false;
3229 2174 });
3230 2175
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);
2176 + // Real-time email validation
2177 + const emailInput = document.getElementById('user-email');
2178 + if (emailInput) {
2179 + let validationTimeout;
2180 +
2181 + emailInput.addEventListener('input', function() {
2182 + // Clear previous validation timeout
2183 + if (validationTimeout) {
2184 + clearTimeout(validationTimeout);
2185 + }
2186 +
2187 + // Debounce validation
2188 + validationTimeout = setTimeout(() => {
2189 + const email = this.value.trim();
2190 + clearEmailError();
2191 +
2192 + if (email && !isValidEmail(email)) {
2193 + showEmailError('Please enter a valid email address.');
2194 + }
2195 + }, 500);
2196 + });
3235 2197
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.');
2198 + // Handle Enter key
2199 + emailInput.addEventListener('keypress', function(e) {
2200 + if (e.key === 'Enter' && !isSubmitting) {
2201 + emailForm.dispatchEvent(new Event('submit'));
3246 2202 }
3247 - }, 500);
2203 + });
2204 + }
3248 2205
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 - }
2206 + // Error display functions
2207 + function showEmailError(message) {
2208 + clearEmailError();
2209 +
2210 + const errorDiv = document.createElement('div');
2211 + errorDiv.className = 'email-error';
2212 + errorDiv.style.cssText = `
2213 + color: #e74c3c;
2214 + font-size: 12px;
2215 + margin-top: 8px;
2216 + padding: 4px 0;
2217 + animation: fadeInError 0.3s ease;
2218 + `;
2219 + errorDiv.textContent = message;
2220 +
2221 + // Add CSS animation if not already present
2222 + if (!document.getElementById('email-error-styles')) {
2223 + const style = document.createElement('style');
2224 + style.id = 'email-error-styles';
2225 + style.textContent = `
2226 + @keyframes fadeInError {
2227 + from { opacity: 0; transform: translateY(-5px); }
2228 + to { opacity: 1; transform: translateY(0); }
2229 + }
2230 + .email-input-shake {
2231 + animation: shake 0.5s ease-in-out;
2232 + }
2233 + @keyframes shake {
2234 + 0%, 100% { transform: translateX(0); }
2235 + 25% { transform: translateX(-5px); }
2236 + 75% { transform: translateX(5px); }
2237 + }
2238 + `;
2239 + document.head.appendChild(style);
3260 2240 }
3261 - });
3262 -
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 - }
2241 +
2242 + emailForm.appendChild(errorDiv);
2243 +
2244 + // Add shake animation to input
2245 + if (emailInput) {
2246 + emailInput.classList.add('email-input-shake');
2247 + setTimeout(() => {
2248 + emailInput.classList.remove('email-input-shake');
2249 + }, 500);
3271 2250 }
3272 - });
2251 + }
3273 2252
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');
2253 + function clearEmailError() {
2254 + const existingErrors = emailForm.querySelectorAll('.email-error');
2255 + existingErrors.forEach(error => error.remove());
2256 + }
3280 2257
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 - }
2258 + // Initialize email check with delay to prevent race conditions
2259 + setTimeout(checkSessionAndEmail, 100);
2260 +
2261 +} else if (mxchatChat.email_collection_enabled) {
2262 + console.error('Essential elements for email handling are missing:', {
2263 + emailForm: !!emailForm,
2264 + emailBlocker: !!emailBlocker,
2265 + chatbotWrapper: !!chatbotWrapper
3295 2266 });
3296 2267 }
3297 2268
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);
2269 +
2270 + // Open chatbot when pre-chat message is clicked
2271 + $(document).on('click', '#pre-chat-message', function() {
2272 + var chatbot = $('#floating-chatbot');
2273 + if (chatbot.hasClass('hidden')) {
2274 + chatbot.removeClass('hidden').addClass('visible');
2275 + $('#floating-chatbot-button').addClass('hidden');
2276 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
3306 2277 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 2278 }
3324 2279 });
3325 2280
3326 - // Legacy duplicate close handler removed — handled by single event delegation above
2281 + var closeButton = document.querySelector('.close-pre-chat-message');
2282 + if (closeButton) {
2283 + closeButton.addEventListener('click', function() {
2284 + $('#pre-chat-message').fadeOut(200); // Hide the message
3327 2285
2286 + // Send an AJAX request to set the transient flag for 24 hours
2287 + $.ajax({
2288 + url: mxchatChat.ajax_url,
2289 + type: 'POST',
2290 + data: {
2291 + action: 'mxchat_dismiss_pre_chat_message',
2292 + _ajax_nonce: mxchatChat.nonce
2293 + },
2294 + success: function() {
2295 + //console.log('Pre-chat message dismissed for 24 hours.');
3328 2296
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');
2297 + // Ensure the message is hidden after dismissal
2298 + $('#pre-chat-message').hide();
2299 + },
2300 + error: function() {
2301 + ////console.error('Failed to dismiss pre-chat message.');
2302 + }
2303 + });
2304 + });
2305 + }
2306 +
2307 +
2308 +function hasQuickQuestions() {
2309 + const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
3334 2310 return questionButtons.length > 0;
3335 2311 }
3336 2312
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)) {
2313 +function collapseQuickQuestions() {
2314 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2315 + if (questionsContainer && hasQuickQuestions()) {
3351 2316 questionsContainer.classList.add('collapsed');
3352 2317 questionsContainer.classList.add('has-been-collapsed');
3353 2318 try {
3354 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3355 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
2319 + sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2320 + sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
3356 2321 } catch (e) {
3357 2322 // Ignore if sessionStorage is not available
3358 2323 }
3359 2324 }
@@ -3358,15 +2323,14 @@
3358 2323 }
3359 2324 }
3360 2325 }
3361 2326
3362 -function expandQuickQuestions(botId) {
3363 - botId = botId || 'default';
3364 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3365 - if (questionsContainer && hasQuickQuestions(botId)) {
2327 +function expandQuickQuestions() {
2328 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2329 + if (questionsContainer && hasQuickQuestions()) {
3366 2330 questionsContainer.classList.remove('collapsed');
3367 2331 try {
3368 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
2332 + sessionStorage.setItem('mxchat_questions_collapsed', 'false');
3369 2333 } catch (e) {
3370 2334 // Ignore if sessionStorage is not available
3371 2335 }
3372 2336 }
@@ -3371,24 +2335,18 @@
3371 2335 }
3372 2336 }
3373 2337 }
3374 2338
3375 -function checkQuickQuestionsState(botId) {
3376 - botId = botId || 'default';
3377 - if (!hasQuickQuestions(botId)) {
2339 +function checkQuickQuestionsState() {
2340 + if (!hasQuickQuestions()) {
3378 2341 return; // Don't do anything if no questions exist
3379 2342 }
3380 -
3381 - // Skip restoring collapsed state for embedded bots - they should always start expanded
3382 - if (isEmbeddedBot(botId)) {
3383 - return;
3384 - }
3385 -
2343 +
3386 2344 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');
2345 + const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2346 + const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2347 +
2348 + const questionsContainer = document.getElementById('mxchat-popular-questions');
3391 2349 if (questionsContainer) {
3392 2350 if (hasBeenCollapsed === 'true') {
3393 2351 questionsContainer.classList.add('has-been-collapsed');
3394 2352 }
@@ -3400,37 +2358,33 @@
3400 2358 // Ignore if sessionStorage is not available
3401 2359 }
3402 2360 }
3403 2361
3404 -// 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) {
2362 +// NEW: Global delegation for dynamically added links as fallback
2363 +$(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
3407 2364 const $link = $(this);
3408 2365 const messageDiv = $link.closest('.bot-message, .agent-message');
3409 -
2366 +
3410 2367 // Only process bot/agent message links
3411 2368 if (messageDiv.length > 0) {
3412 2369 const originalHref = $link.attr('href');
3413 -
2370 +
3414 2371 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3415 2372 e.preventDefault();
3416 2373 e.stopPropagation();
3417 -
2374 +
3418 2375 // Mark as tracked
3419 2376 $link.attr('data-tracked', 'true');
3420 -
3421 - // Get bot ID from the chat box context
3422 - var botId = getBotIdFromElement(this);
3423 -
2377 +
3424 2378 // Get message context from the message div
3425 2379 const messageText = messageDiv.text().substring(0, 200);
3426 -
2380 +
3427 2381 $.ajax({
3428 2382 url: mxchatChat.ajax_url,
3429 2383 type: 'POST',
3430 2384 data: {
3431 2385 action: 'mxchat_track_url_click',
3432 - session_id: getChatSession(botId),
2386 + session_id: getChatSession(),
3433 2387 url: originalHref,
3434 2388 message_context: messageText,
3435 2389 nonce: mxchatChat.nonce
3436 2390 },
@@ -3441,58 +2395,45 @@
3441 2395 window.location.href = originalHref;
3442 2396 }
3443 2397 }
3444 2398 });
3445 -
2399 +
3446 2400 return false;
3447 2401 }
3448 2402 }
3449 2403 });
3450 2404
3451 - // ====================================
3452 - // MAIN INITIALIZATION
3453 - // ====================================
2405 +
2406 +
2407 +
2408 +// ====================================
2409 +// MAIN INITIALIZATION
2410 +// ====================================
3454 2411
3455 - // Initialize all chatbot instances on the page
3456 - initializeAllInstances();
2412 +if ($('#floating-chatbot').hasClass('hidden')) {
2413 + $('#floating-chatbot-button').removeClass('hidden');
2414 +}
2415 +// Initialize when document is ready
2416 +setFullHeight();
2417 +initializeChatVisibility();
2418 +loadChatHistory();
2419 +trackOriginatingPage();
3457 2420
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 - });
2421 +// Make functions globally available for add-ons
2422 +window.hasQuickQuestions = hasQuickQuestions;
2423 +window.collapseQuickQuestions = collapseQuickQuestions;
2424 +window.appendMessage = appendMessage;
2425 +window.appendThinkingMessage = appendThinkingMessage;
2426 +window.scrollToBottom = scrollToBottom;
2427 +window.scrollElementToTop = scrollElementToTop;
2428 +window.replaceLastMessage = replaceLastMessage;
2429 +window.callMxChat = callMxChat;
2430 +window.callMxChatStream = callMxChatStream;
2431 +window.shouldUseStreaming = shouldUseStreaming;
2432 +window.getChatSession = getChatSession;
2433 +window.getPageContext = getPageContext;
2434 +window.updateStreamingMessage = updateStreamingMessage;
3463 2435
3464 - // Initialize when document is ready
3465 - setFullHeight();
3466 -
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;
3494 -
3495 2436 }); // End of jQuery ready
3496 2437
3497 2438
3498 2439 // ====================================
@@ -3520,5 +2461,10 @@
3520 2461 });
3521 2462 }
3522 2463 }
3523 2464 });
2465 +
2466 +
2467 +
2468 +
2469 +
3524 2470