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