PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.4
MxChat – AI Chatbot & Content Generation for WordPress v2.3.4
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 +1193 -2623 3.2.52.3.4 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,156 +169,73 @@
396 169 content: pageContent
397 170 };
398 171 }
399 172
400 -// Track originating page when chat starts
401 -function trackOriginatingPage() {
402 - const sessionId = getChatSession();
403 - const pageUrl = window.location.href;
404 - const pageTitle = document.title || 'Untitled Page';
405 -
406 - // Only track once per session
407 - const trackingKey = 'mxchat_originating_tracked_' + sessionId;
408 - if (sessionStorage.getItem(trackingKey)) {
409 - return;
410 - }
411 -
412 - $.ajax({
413 - url: mxchatChat.ajax_url,
414 - type: 'POST',
415 - data: {
416 - action: 'mxchat_track_originating_page',
417 - session_id: sessionId,
418 - page_url: pageUrl,
419 - page_title: pageTitle,
420 - nonce: mxchatChat.nonce
421 - },
422 - success: function(response) {
423 - if (response.success) {
424 - sessionStorage.setItem(trackingKey, 'true');
425 - }
426 - }
427 - });
428 -}
429 -
430 173 // ====================================
431 174 // CORE CHAT FUNCTIONALITY
432 175 // ====================================
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 176 // 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 -
177 +function sendMessage() {
178 + var message = $('#chat-input').val();
179 +
473 180 // ADD PROMPT HOOK HERE
474 - if (typeof customMxChatFilter === 'function') {
475 - message = customMxChatFilter(message, "prompt");
181 + if (typeof customMxChatFilter === 'function') {
182 + message = customMxChatFilter(message, "prompt");
476 183 }
477 -
184 +
478 185 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 - }
186 + appendMessage("user", message);
187 + $('#chat-input').val('');
188 + $('#chat-input').css('height', 'auto');
485 189
486 - appendMessage("user", message, '', [], false, botId);
487 - $chatInput.val('');
488 - $chatInput.css('height', 'auto');
489 -
490 - if (hasQuickQuestions(botId)) {
491 - collapseQuickQuestions(botId);
190 + if (hasQuickQuestions()) {
191 + collapseQuickQuestions();
492 192 }
493 - appendThinkingMessage(botId);
494 - scrollToBottom(botId);
193 + appendThinkingMessage();
194 + scrollToBottom();
495 195
496 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
196 + const currentModel = mxchatChat.model || 'gpt-4o';
497 197
498 198 // Check if streaming is enabled AND supported for this model
499 199 if (shouldUseStreaming(currentModel)) {
500 200 callMxChatStream(message, function(response) {
501 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
502 - }, botId);
201 + $('.bot-message.temporary-message').removeClass('temporary-message');
202 + });
503 203 } else {
504 204 callMxChat(message, function(response) {
505 - replaceLastMessage("bot", response, '', [], botId);
506 - }, botId);
205 + replaceLastMessage("bot", response);
206 + });
507 207 }
508 208 }
509 209 }
510 210
511 211 // Update your existing sendMessageToChatbot function
512 -function sendMessageToChatbot(message, botId) {
513 - botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 -
212 +function sendMessageToChatbot(message) {
516 213 // ADD PROMPT HOOK HERE
517 214 if (typeof customMxChatFilter === 'function') {
518 215 message = customMxChatFilter(message, "prompt");
519 216 }
217 +
218 + var sessionId = getChatSession();
520 219
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);
220 + if (hasQuickQuestions()) {
221 + collapseQuickQuestions();
526 222 }
223 + appendThinkingMessage();
224 + scrollToBottom();
527 225
528 - var sessionId = getChatSession(botId);
226 + const currentModel = mxchatChat.model || 'gpt-4o';
529 227
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 228 // Check if streaming is enabled AND supported for this model
539 229 if (shouldUseStreaming(currentModel)) {
540 230 callMxChatStream(message, function(response) {
541 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
542 - }, botId);
231 + $('.bot-message.temporary-message').removeClass('temporary-message');
232 + });
543 233 } else {
544 234 callMxChat(message, function(response) {
545 - getElement(botId, 'chat-box').find('.temporary-message').remove();
546 - replaceLastMessage("bot", response, '', [], botId);
547 - }, botId);
235 + $('.temporary-message').remove();
236 + replaceLastMessage("bot", response);
237 + });
548 238 }
549 239 }
550 240
551 241 // Updated shouldUseStreaming function with debugging
@@ -560,81 +250,18 @@
560 250 // Only use streaming if both enabled and supported
561 251 return streamingEnabled && streamingSupported;
562 252 }
563 253
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 -
254 +function callMxChat(message, callback) {
610 255 // Get page context if contextual awareness is enabled
611 256 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 -
257 +
626 258 // Prepare AJAX data
627 259 const ajaxData = {
628 260 action: 'mxchat_handle_chat_request',
629 261 message: message,
630 - session_id: sessionId,
631 - 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
262 + session_id: getChatSession(),
263 + nonce: mxchatChat.nonce
637 264 };
638 265
639 266 // Add page context if available
640 267 if (pageContext) {
@@ -657,98 +284,37 @@
657 284 type: 'POST',
658 285 dataType: 'json',
659 286 data: ajaxData,
660 287 success: function(response) {
661 - // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
662 - if (response.chat_mode) {
663 - updateChatModeIndicator(response.chat_mode, botId);
664 - }
288 + // Log the full response for debugging
289 + //console.log("API Response:", response);
665 290
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
291 + // First check if this is a successful response by looking for text, html, or message fields
729 292 // This preserves compatibility with your server response format
730 - if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
293 + if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
731 294 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
732 295
733 296 // Handle successful response - this is your original success handling code
734 297
735 - // Handle other responses
736 - let responseText = response.text || '';
737 - let responseHtml = response.html || '';
738 - let responseMessage = response.message || '';
298 + // Existing chat mode check
299 + if (response.chat_mode) {
300 + updateChatModeIndicator(response.chat_mode);
301 + }
302 + else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
303 + updateChatModeIndicator(response.fallbackResponse.chat_mode);
304 + }
739 305
740 306 // Add PDF filename handling
741 307 if (response.data && response.data.filename) {
742 - showActivePdf(response.data.filename, botId);
743 - var instance = MxChatInstances.get(botId);
744 - instance.activePdfFile = response.data.filename;
308 + showActivePdf(response.data.filename);
309 + activePdfFile = response.data.filename;
745 310 }
746 311
747 312 // Add redirect check here
748 313 if (response.redirect_url) {
314 + let responseText = response.text || '';
749 315 if (responseText) {
750 - replaceLastMessage("bot", responseText, '', [], botId);
316 + replaceLastMessage("bot", responseText);
751 317 }
752 318 setTimeout(() => {
753 319 window.location.href = response.redirect_url;
754 320 }, 1500);
@@ -756,17 +322,24 @@
756 322 }
757 323
758 324 // Check for live agent response
759 325 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 - removeThinkingDots(botId);
761 - updateChatModeIndicator('agent', botId);
762 - enableChatInput(botId);
326 + updateChatModeIndicator('agent');
763 327 return;
764 328 }
765 329
330 + // Handle other responses
331 + let responseText = response.text || '';
332 + let responseHtml = response.html || '';
333 + let responseMessage = response.message || '';
334 +
335 + if (responseText === 'You are now chatting with the AI chatbot.') {
336 + updateChatModeIndicator('ai');
337 + }
338 +
766 339 // Handle the message and show notification if chat is hidden
767 340 if (responseText || responseHtml || responseMessage) {
768 -
341 +
769 342 // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
770 343 if (responseText && typeof customMxChatFilter === 'function') {
771 344 responseText = customMxChatFilter(responseText, "response");
772 345 }
@@ -772,53 +345,89 @@
772 345 }
773 346 if (responseMessage && typeof customMxChatFilter === 'function') {
774 347 responseMessage = customMxChatFilter(responseMessage, "response");
775 348 }
776 -
349 +
777 350 // Update the messages as before
778 351 if (responseText && responseHtml) {
779 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
352 + replaceLastMessage("bot", responseText, responseHtml);
780 353 } else if (responseText) {
781 - replaceLastMessage("bot", responseText, '', [], botId);
354 + replaceLastMessage("bot", responseText);
782 355 } else if (responseHtml) {
783 - replaceLastMessage("bot", "", responseHtml, [], botId);
356 + replaceLastMessage("bot", "", responseHtml);
784 357 } else if (responseMessage) {
785 - replaceLastMessage("bot", responseMessage, '', [], botId);
358 + replaceLastMessage("bot", responseMessage);
786 359 }
787 360
788 361 // 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();
362 + if ($('#floating-chatbot').hasClass('hidden')) {
363 + const badge = $('#chat-notification-badge');
364 + if (badge.length) {
365 + badge.show();
794 366 }
795 367 }
796 368 } 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);
369 + ////console.error("Unexpected response format:", response);
370 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
802 371 }
803 372
804 373 if (response.message_id) {
805 - var instance = MxChatInstances.get(botId);
806 - instance.lastSeenMessageId = response.message_id;
374 + lastSeenMessageId = response.message_id;
807 375 }
808 376
809 377 return;
810 378 }
811 379
812 - // Fallback for truly unexpected response formats
813 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
380 + // If we got here, it's likely an error response
381 + // Now we can check for error conditions with our robust error handling
382 +
383 + let errorMessage = "";
384 + let errorCode = "";
385 +
386 + // Check various possible error locations in the response
387 + if (response.data && response.data.error_message) {
388 + errorMessage = response.data.error_message;
389 + errorCode = response.data.error_code || "";
390 + } else if (response.error_message) {
391 + errorMessage = response.error_message;
392 + errorCode = response.error_code || "";
393 + } else if (response.message) {
394 + errorMessage = response.message;
395 + } else if (typeof response.data === 'string') {
396 + errorMessage = response.data;
397 + } else if (!response.success) {
398 + // Explicit check for success: false without other error info
399 + errorMessage = "An error occurred. Please try again or contact support.";
400 + } else {
401 + // Fallback for any other unexpected response format
402 + errorMessage = "Unexpected response received. Please try again or contact support.";
403 + }
404 +
405 + // Log the error with code for debugging
406 + //console.log("Response data:", response.data);
407 + ////console.error("API Error:", errorMessage, "Code:", errorCode);
408 +
409 + // Format user-friendly error message
410 + let displayMessage = errorMessage;
411 +
412 + // Customize message for admin users
413 + if (mxchatChat.is_admin) {
414 + // For admin users, show more technical details including error code
415 + displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
416 + }
417 +
418 + replaceLastMessage("bot", displayMessage);
814 419 },
815 420 error: function(xhr, status, error) {
421 + //console.error("AJAX Error:", status, error);
422 + //console.log("Response Text:", xhr.responseText);
423 +
816 424 let errorMessage = "An unexpected error occurred.";
817 425
818 426 // Try to parse the response if it's JSON
819 427 try {
820 428 const responseJson = JSON.parse(xhr.responseText);
429 + //console.log("Parsed error response:", responseJson);
821 430
822 431 if (responseJson.data && responseJson.data.error_message) {
823 432 errorMessage = responseJson.data.error_message;
824 433 } else if (responseJson.message) {
@@ -838,22 +447,20 @@
838 447 errorMessage = "Server error: The server encountered an issue. Please try again later.";
839 448 }
840 449 }
841 450
842 - replaceLastMessage("bot", errorMessage, '', [], botId);
451 + replaceLastMessage("bot", errorMessage);
843 452 }
844 453 });
845 454 }
846 455
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';
456 +function callMxChatStream(message, callback) {
457 + //console.log("Using streaming for message:", message);
458 +
459 + const currentModel = mxchatChat.model || 'gpt-4o';
854 460 if (!isStreamingSupported(currentModel)) {
855 - callMxChat(message, callback, botId);
461 + //console.log("Streaming not supported, falling back to regular call");
462 + callMxChat(message, callback);
856 463 return;
857 464 }
858 465
859 466 // Get page context if contextual awareness is enabled
@@ -858,32 +465,14 @@
858 465
859 466 // Get page context if contextual awareness is enabled
860 467 const pageContext = getPageContext();
861 468
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 469 const formData = new FormData();
876 470 formData.append('action', 'mxchat_stream_chat');
877 471 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
472 + formData.append('session_id', getChatSession());
879 473 formData.append('nonce', mxchatChat.nonce);
880 - formData.append('current_page_url', window.location.href);
881 - 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 -
474 +
886 475 // Add page context if available
887 476 if (pageContext) {
888 477 formData.append('page_context', JSON.stringify(pageContext));
889 478 }
@@ -900,9 +489,8 @@
900 489 }
901 490
902 491 let accumulatedContent = '';
903 492 let testingDataReceived = false;
904 - let streamingStarted = false;
905 493
906 494 fetch(mxchatChat.ajax_url, {
907 495 method: 'POST',
908 496 body: formData,
@@ -908,116 +496,135 @@
908 496 body: formData,
909 497 credentials: 'same-origin'
910 498 })
911 499 .then(response => {
912 - // Store the response for potential fallback handling
913 - const responseClone = response.clone();
914 -
500 + //console.log("Streaming response received:", response);
501 +
915 502 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 - });
503 + throw new Error('Network response was not ok');
922 504 }
923 505
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 - }
932 -
933 - // Check for testing panel
934 - if (window.mxchatTestPanelInstance && data.testing_data) {
935 - window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
936 - }
937 -
938 - // Handle the JSON response directly
939 - handleNonStreamResponse(data, callback, botId);
940 - return Promise.resolve(); // Prevent further processing
941 - });
942 - }
943 -
944 - // Continue with streaming processing
945 - const reader = response.body.getReader();
946 - const decoder = new TextDecoder();
947 - let buffer = '';
948 -
949 - function processStream() {
950 - reader.read().then(({ done, value }) => {
951 - 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;
506 + // Check if response is JSON instead of streaming
507 + const contentType = response.headers.get('content-type');
508 + if (contentType && contentType.includes('application/json')) {
509 + //console.log("Received JSON response instead of stream, handling as regular response");
510 + return response.json().then(data => {
511 + // Handle as regular JSON response
512 + $('.bot-message.temporary-message').remove();
513 +
514 + // FIXED: Always check for testing panel, not just in testing mode
515 + if (window.mxchatTestPanelInstance && data.testing_data) {
516 + //console.log('Testing data found in streaming JSON response:', data.testing_data);
517 + window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
518 + }
519 +
520 + // Handle different response formats (including intent responses)
521 + if (data.text || data.html || data.message) {
522 +
523 + // ADD RESPONSE HOOKS HERE - FOR STREAMING JSON RESPONSES
524 + if (data.text && typeof customMxChatFilter === 'function') {
525 + data.text = customMxChatFilter(data.text, "response");
975 526 }
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);
527 + if (data.message && typeof customMxChatFilter === 'function') {
528 + data.message = customMxChatFilter(data.message, "response");
986 529 }
530 +
531 + if (data.text && data.html) {
532 + replaceLastMessage("bot", data.text, data.html);
533 + } else if (data.text) {
534 + replaceLastMessage("bot", data.text);
535 + } else if (data.html) {
536 + replaceLastMessage("bot", "", data.html);
537 + } else if (data.message) {
538 + replaceLastMessage("bot", data.message);
539 + }
540 + }
541 +
542 + // Handle other response properties
543 + if (data.chat_mode) {
544 + updateChatModeIndicator(data.chat_mode);
545 + }
546 +
547 + if (data.data && data.data.filename) {
548 + showActivePdf(data.data.filename);
549 + activePdfFile = data.data.filename;
550 + }
551 +
552 + if (callback) {
553 + callback(data.text || data.message || '');
554 + }
555 + });
556 + }
987 557
988 - if (callback) {
989 - callback(accumulatedContent);
558 + // NEW: Check if it's event-stream but might contain JSON (Cloudflare issue)
559 + if (contentType && contentType.includes('text/event-stream')) {
560 + // First, try to read the entire response to check if it's actually JSON
561 + return response.text().then(fullText => {
562 + // Check if the response looks like a single JSON object instead of SSE format
563 + const trimmedText = fullText.trim();
564 + if (trimmedText.startsWith('{') && trimmedText.endsWith('}') && !trimmedText.includes('data: ')) {
565 + //console.log("Detected JSON in event-stream wrapper, parsing as JSON fallback");
566 + try {
567 + const data = JSON.parse(trimmedText);
568 +
569 + // Handle as regular JSON response
570 + $('.bot-message.temporary-message').remove();
571 +
572 + // Check for testing panel data
573 + if (window.mxchatTestPanelInstance && data.testing_data) {
574 + window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
575 + }
576 +
577 + // Handle response content
578 + if (data.text || data.html || data.message) {
579 + if (data.text && typeof customMxChatFilter === 'function') {
580 + data.text = customMxChatFilter(data.text, "response");
581 + }
582 + if (data.message && typeof customMxChatFilter === 'function') {
583 + data.message = customMxChatFilter(data.message, "response");
584 + }
585 +
586 + if (data.text && data.html) {
587 + replaceLastMessage("bot", data.text, data.html);
588 + } else if (data.text) {
589 + replaceLastMessage("bot", data.text);
590 + } else if (data.html) {
591 + replaceLastMessage("bot", "", data.html);
592 + } else if (data.message) {
593 + replaceLastMessage("bot", data.message);
594 + }
595 + }
596 +
597 + // Handle other response properties
598 + if (data.chat_mode) {
599 + updateChatModeIndicator(data.chat_mode);
600 + }
601 +
602 + if (data.data && data.data.filename) {
603 + showActivePdf(data.data.filename);
604 + activePdfFile = data.data.filename;
605 + }
606 +
607 + if (callback) {
608 + callback(data.text || data.message || '');
609 + }
610 +
611 + return; // Exit early, we handled it as JSON
612 + } catch (e) {
613 + //console.log("Failed to parse as JSON, continuing with streaming logic");
990 614 }
991 - return;
992 615 }
993 -
994 - buffer += decoder.decode(value, { stream: true });
995 - const lines = buffer.split('\n');
996 - buffer = lines.pop() || '';
997 -
616 +
617 + // If we get here, it's actual streaming data, process it normally
618 + const lines = fullText.split('\n');
619 + let buffer = '';
620 +
998 621 for (const line of lines) {
999 622 if (line.startsWith('data: ')) {
1000 623 const data = line.substring(6);
1001 624
1002 625 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 -
626 + //console.log("Received [DONE] signal");
1020 627 if (callback) {
1021 628 callback(accumulatedContent);
1022 629 }
1023 630 return;
@@ -1024,16 +631,12 @@
1024 631 }
1025 632
1026 633 try {
1027 634 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 -
635 +
1034 636 // Handle testing data
1035 637 if (json.testing_data && !testingDataReceived) {
638 + //console.log('Testing data received in stream:', json.testing_data);
1036 639 if (window.mxchatTestPanelInstance) {
1037 640 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
1038 641 testingDataReceived = true;
1039 642 }
@@ -1039,1166 +642,369 @@
1039 642 }
1040 643 }
1041 644 // Handle content streaming
1042 645 else if (json.content) {
1043 - streamingStarted = true;
1044 646 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 - }
647 + updateStreamingMessage(accumulatedContent);
648 + }
1052 649 // Handle errors
1053 650 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 - }
651 + //console.error("Streaming error:", json.error);
652 + replaceLastMessage("bot", "Error: " + json.error);
1068 653 return;
1069 654 }
1070 655 } catch (e) {
1071 - // SSE data parsing error - silently continue
656 + //console.error('Error parsing SSE data:', e, 'Data:', data);
1072 657 }
1073 658 }
1074 659 }
1075 -
1076 - processStream();
1077 - }).catch(streamError => {
1078 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1079 - callMxChat(message, callback, botId);
660 +
661 + if (callback) {
662 + callback(accumulatedContent);
663 + }
1080 664 });
1081 665 }
1082 666
1083 - processStream();
667 + // If we get here, something unexpected happened, fall back to regular processing
668 + //console.log("Unexpected content type, falling back to text processing");
669 + return response.text().then(text => {
670 + //console.log("Raw response text:", text);
671 + // Try to extract any meaningful content and display it
672 + replaceLastMessage("bot", "Received response but couldn't process streaming format. Please try again.");
673 + });
1084 674 })
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 - });
675 + .catch(error => {
676 + //console.error('Streaming error:', error);
677 + // Fall back to regular chat on any error
678 + callMxChat(message, callback);
679 + });
1100 680 }
1101 681
1102 -// Helper function to handle non-streaming responses
1103 -function handleNonStreamResponse(data, callback, botId) {
1104 - botId = botId || 'default';
1105 682
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 683 // Function to update message during streaming
1267 -function updateStreamingMessage(content, botId) {
1268 - botId = botId || 'default';
1269 -
684 +function updateStreamingMessage(content) {
1270 685 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1271 686 if (typeof customMxChatFilter === 'function') {
1272 687 content = customMxChatFilter(content, "response");
1273 688 }
689 +
690 + const formattedContent = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(content))));
1274 691
1275 - const formattedContent = linkify(content);
692 + // Find the temporary message
693 + const tempMessage = $('.bot-message.temporary-message').last();
1276 694
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 695 if (tempMessage.length) {
1282 696 // Update existing message
1283 697 tempMessage.html(formattedContent);
1284 698 } else {
1285 699 // Create new temporary message if it doesn't exist
1286 - appendMessage("bot", content, '', [], true, botId);
700 + appendMessage("bot", content, '', [], true);
1287 701 }
1288 702 }
1289 703
704 +// UPGRADE: Function to check if streaming is supported for the current model
1290 705 function isStreamingSupported(model) {
1291 706 if (!model) return false;
1292 707
708 + //console.log("Checking streaming support for model:", model); // Debug log
709 +
710 + // Get the model prefix
1293 711 const modelPrefix = model.split('-')[0].toLowerCase();
712 +
713 + //console.log("Model prefix:", modelPrefix); // Debug log
1294 714
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
715 + // Support streaming for OpenAI, Claude, and Grok models
716 + const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude' || modelPrefix === 'grok';
1302 717
718 + //console.log("Streaming supported:", isSupported); // Debug log
719 +
1303 720 return isSupported;
1304 721 }
1305 722
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);
723 +// Update the event handlers to use the correct function names
724 +$('#send-button').off('click').on('click', function() {
725 + sendMessage(); // Use the updated sendMessage function
1315 726 });
1316 727
1317 -// Override enter key handler (using event delegation)
1318 -$(document).on('keypress', '.chat-input', function(e) {
728 +// Override enter key handler
729 +$('#chat-input').off('keypress').on('keypress', function(e) {
1319 730 if (e.which == 13 && !e.shiftKey) {
1320 731 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);
732 + sendMessage(); // Use the updated sendMessage function
1327 733 }
1328 734 });
1329 735
1330 736
1331 -// Tags the chat-box transcript so the print stylesheet can target it,
1332 -// and lazily initializes the header overflow menu for this bot if the
1333 -// markup is present but not yet wired (covers dynamically-rendered widgets).
1334 -// Idempotent; safe to call on every appended message.
1335 -function mxchatEnsurePrintRoot(botId) {
1336 - var $chatBox = getElement(botId, 'chat-box');
1337 - if (!$chatBox || !$chatBox.length) return;
1338 - $chatBox.addClass('mxchat-conversation-print-root');
1339 - if (!$chatBox.attr('data-print-title')) {
1340 - var nowStr = new Date().toLocaleString();
1341 - var headerTitle = ((typeof mxchatChat !== 'undefined' && mxchatChat.print_header_title) || 'Chat transcript') + ' — ' + nowStr;
1342 - $chatBox.attr('data-print-title', headerTitle);
1343 - }
1344 - if (typeof mxchatInitHeaderMenu === 'function') {
1345 - mxchatInitHeaderMenu(botId);
1346 - }
1347 -}
1348 -
1349 -// Builds the list of overflow-menu items for a given bot.
1350 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1351 -function mxchatGetHeaderMenuItems(botId) {
1352 - var items = [];
1353 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1354 -
1355 - // The `print_button_*` keys still gate this item for back-compat with
1356 - // existing user options. The action is now a transcript download, not print.
1357 - if (settings.print_button_enabled === 'on') {
1358 - items.push({
1359 - id: 'download-transcript',
1360 - label: settings.print_button_label || 'Download Transcript',
1361 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1362 - action: function() {
1363 - mxchatDownloadTranscript(botId);
737 + function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
738 + try {
739 + // Determine styles based on sender type
740 + let messageClass, bgColor, fontColor;
741 +
742 + if (sender === "user") {
743 + messageClass = "user-message";
744 + bgColor = userMessageBgColor;
745 + fontColor = userMessageFontColor;
746 + // Only sanitize user input
747 + messageText = sanitizeUserInput(messageText);
748 + } else if (sender === "agent") {
749 + messageClass = "agent-message";
750 + bgColor = liveAgentMessageBgColor;
751 + fontColor = liveAgentMessageFontColor;
752 + } else {
753 + messageClass = "bot-message";
754 + bgColor = botMessageBgColor;
755 + fontColor = botMessageFontColor;
1364 756 }
1365 - });
757 +
758 + const messageDiv = $('<div>')
759 + .addClass(messageClass)
760 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
761 + .css({
762 + 'background': bgColor,
763 + 'color': fontColor,
764 + 'margin-bottom': '1em'
765 + });
766 +
767 + // Process the message content based on sender
768 + let fullMessage;
769 + if (sender === "user") {
770 + // For user messages, apply linkify after sanitization
771 + fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
772 + } else {
773 + // For bot/agent messages, preserve HTML
774 + fullMessage = messageText;
775 + }
776 +
777 + // Add images if provided
778 + if (images && images.length > 0) {
779 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
780 + images.forEach(img => {
781 + // Ensure image URLs and titles are properly escaped
782 + const safeTitle = sanitizeUserInput(img.title);
783 + const safeUrl = encodeURI(img.image_url);
784 + const safeThumbnail = encodeURI(img.thumbnail_url);
785 +
786 + fullMessage += `
787 + <div style="margin-bottom: 10px;">
788 + <strong>${safeTitle}</strong><br>
789 + <a href="${safeUrl}" target="_blank">
790 + <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
791 + </a>
792 + </div>`;
793 + });
794 + fullMessage += '</div>';
795 + }
796 +
797 + // Append HTML content if provided
798 + if (messageHtml && sender !== "user") {
799 + fullMessage += '<br><br>' + messageHtml;
800 + }
801 +
802 + messageDiv.html(fullMessage);
803 +
804 + if (isTemporary) {
805 + messageDiv.addClass('temporary-message');
806 + }
807 +
808 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
809 + if (sender === "bot") {
810 + const lastUserMessage = $('#chat-box').find('.user-message').last();
811 + if (lastUserMessage.length) {
812 + scrollElementToTop(lastUserMessage);
813 + }
814 + }
815 + });
816 +
817 + if (messageText.id) {
818 + lastSeenMessageId = messageText.id;
819 + hideNotification();
820 + }
821 + } catch (error) {
822 + //console.error("Error rendering message:", error);
823 + }
1366 824 }
1367 -
1368 - return items;
1369 -}
1370 -
1371 -// Builds a clean markdown transcript of the current conversation and triggers
1372 -// a file download. Used by the "Download Transcript" menu item.
1373 -function mxchatDownloadTranscript(botId) {
1374 - var $chatBox = getElement(botId, 'chat-box');
1375 - if (!$chatBox || !$chatBox.length) return;
1376 -
1377 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1378 - var headerTitle = settings.print_header_title || 'Chat transcript';
1379 - var now = new Date();
1380 - var stamp = now.toLocaleString();
1381 -
1382 - var lines = [];
1383 - lines.push('# ' + headerTitle);
1384 - lines.push('');
1385 - lines.push('Exported: ' + stamp);
1386 - lines.push('');
1387 - lines.push('---');
1388 - lines.push('');
1389 -
1390 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1391 - var $msg = $(this);
1392 - // Skip thinking placeholders and any in-flight temporary messages.
1393 - if ($msg.find('.thinking-dots').length) return;
1394 - if ($msg.hasClass('temporary-message')) return;
1395 -
1396 - var sender;
1397 - if ($msg.hasClass('user-message')) sender = 'User';
1398 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1399 - else sender = 'AI Agent';
1400 -
1401 - // Strip interactive UI from the cloned message so we get the conversation text.
1402 - var $clone = $msg.clone();
1403 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1404 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1405 - if (!text) return;
1406 -
1407 - lines.push('**' + sender + '**');
1408 - lines.push('');
1409 - lines.push(text);
1410 - lines.push('');
1411 - });
1412 -
1413 - var content = lines.join('\n');
1414 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1415 - var fname = 'mxchat-transcript-' + iso + '.md';
1416 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1417 - var url = URL.createObjectURL(blob);
1418 - var a = document.createElement('a');
1419 - a.href = url;
1420 - a.download = fname;
1421 - a.style.display = 'none';
1422 - document.body.appendChild(a);
1423 - a.click();
1424 - setTimeout(function() {
1425 - if (a.parentNode) a.parentNode.removeChild(a);
1426 - URL.revokeObjectURL(url);
1427 - }, 100);
1428 -}
1429 -
1430 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1431 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1432 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1433 -function mxchatSyncMenuColors(botId, $wrap) {
1434 - if (!$wrap || !$wrap.length) return;
1435 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1436 - if (!$bot.length) return;
1437 - var cs = window.getComputedStyle($bot[0]);
1438 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1439 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1440 - }
1441 - // Bot text color usually lives on a child div, not .bot-message itself.
1442 - var $textChild = $bot.find('[style*="color"]').first();
1443 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1444 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1445 -}
1446 -
1447 -// One-time per-widget init: renders menu items, wires open/close,
1448 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1449 -function mxchatInitHeaderMenu(botId) {
1450 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1451 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1452 -
1453 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1454 - var $menu = $wrap.find('.mxchat-header-menu');
1455 - var items = mxchatGetHeaderMenuItems(botId);
1456 -
1457 - // Initial color sync — covers normal page load.
1458 - mxchatSyncMenuColors(botId, $wrap);
1459 -
1460 - if (!items.length) {
1461 - $trigger.hide();
1462 - $menu.hide();
1463 - $wrap.data('mxchatMenuReady', true);
1464 - return;
1465 - }
1466 -
1467 - // Build the menu items.
1468 - $menu.empty();
1469 - items.forEach(function(item, idx) {
1470 - var $btn = $('<button>', {
1471 - type: 'button',
1472 - 'class': 'mxchat-menu-item',
1473 - 'role': 'menuitem',
1474 - 'tabindex': '-1',
1475 - 'data-menu-id': item.id,
1476 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1477 - '<span class="mxchat-menu-item-label"></span>'
1478 - });
1479 - $btn.find('.mxchat-menu-item-label').text(item.label);
1480 - $btn.on('click', function(e) {
1481 - e.preventDefault();
1482 - e.stopPropagation();
1483 - closeMenu();
1484 - try { item.action(); } catch (err) { /* no-op */ }
1485 - });
1486 - $menu.append($btn);
1487 - });
1488 -
1489 - function openMenu() {
1490 - // Re-sync each open in case the active theme changed since init.
1491 - mxchatSyncMenuColors(botId, $wrap);
1492 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1493 - $trigger.attr('aria-expanded', 'true');
1494 - // Focus the first item for keyboard users
1495 - setTimeout(function() {
1496 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1497 - }, 0);
1498 - }
1499 - function closeMenu(returnFocus) {
1500 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1501 - $trigger.attr('aria-expanded', 'false');
1502 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1503 - if (returnFocus) $trigger.trigger('focus');
1504 - }
1505 -
1506 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1507 - // click-to-collapse handler does not fire.
1508 - $trigger.on('click', function(e) {
1509 - e.preventDefault();
1510 - e.stopPropagation();
1511 - if ($menu.hasClass('is-open')) closeMenu();
1512 - else openMenu();
1513 - });
1514 -
1515 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1516 - $menu.on('click', function(e) {
1517 - e.stopPropagation();
1518 - });
1519 -
1520 - // Outside click closes the menu.
1521 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1522 - if (!$menu.hasClass('is-open')) return;
1523 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1524 - closeMenu();
1525 - });
1526 -
1527 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1528 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1529 - var $items = $menu.find('.mxchat-menu-item');
1530 - var idx = $items.index(this);
1531 - if (e.key === 'Escape') {
1532 - e.preventDefault();
1533 - closeMenu(true);
1534 - } else if (e.key === 'ArrowDown') {
1535 - e.preventDefault();
1536 - var $next = $items.eq((idx + 1) % $items.length);
1537 - $items.attr('tabindex', '-1');
1538 - $next.attr('tabindex', '0').trigger('focus');
1539 - } else if (e.key === 'ArrowUp') {
1540 - e.preventDefault();
1541 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1542 - $items.attr('tabindex', '-1');
1543 - $prev.attr('tabindex', '0').trigger('focus');
1544 - } else if (e.key === 'Enter' || e.key === ' ') {
1545 - e.preventDefault();
1546 - $(this).trigger('click');
1547 - }
1548 - });
1549 - $trigger.on('keydown', function(e) {
1550 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1551 - e.preventDefault();
1552 - closeMenu(true);
1553 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1554 - e.preventDefault();
1555 - openMenu();
1556 - }
1557 - });
1558 -
1559 - $wrap.data('mxchatMenuReady', true);
1560 -}
1561 -
1562 -// Initialize header menus for every rendered widget on DOM ready.
1563 -$(function() {
1564 - $('.mxchat-header-menu-wrap').each(function() {
1565 - var botId = $(this).data('bot-id');
1566 - if (botId) mxchatInitHeaderMenu(botId);
1567 - });
1568 -});
1569 -
1570 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1571 - try {
1572 - // Determine styles based on sender type
1573 - let messageClass, bgColor, fontColor;
1574 -
825 +
826 + function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
827 + var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
828 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
829 +
830 + // Determine styles
831 + let bgColor, fontColor;
1575 832 if (sender === "user") {
1576 - messageClass = "user-message";
1577 833 bgColor = userMessageBgColor;
1578 834 fontColor = userMessageFontColor;
1579 - // Only sanitize user input
1580 - messageText = sanitizeUserInput(messageText);
1581 835 } else if (sender === "agent") {
1582 - messageClass = "agent-message";
1583 836 bgColor = liveAgentMessageBgColor;
1584 837 fontColor = liveAgentMessageFontColor;
1585 838 } else {
1586 - messageClass = "bot-message";
1587 839 bgColor = botMessageBgColor;
1588 840 fontColor = botMessageFontColor;
1589 841 }
1590 -
1591 - const messageDiv = $('<div>')
1592 - .addClass(messageClass)
1593 - .attr('dir', 'auto');
1594 -
1595 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1596 - var skipColors = shouldSkipInlineColors(botId);
1597 - if (skipColors) {
1598 - messageDiv.css({
1599 - 'margin-bottom': '1em'
1600 - });
1601 - } else {
1602 - messageDiv.css({
1603 - 'background': bgColor,
1604 - 'color': fontColor,
1605 - 'margin-bottom': '1em'
1606 - });
842 +
843 + var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
844 + if (responseHtml) {
845 + fullMessage += '<br><br>' + responseHtml;
1607 846 }
1608 -
1609 - // Process the message content - always run linkify to convert markdown
1610 - // links and format text. linkify() handles existing HTML safely via
1611 - // negative lookaheads that skip URLs already inside <a> tags.
1612 - let fullMessage = linkify(messageText);
1613 -
1614 - // Add images if provided
1615 - if (images && images.length > 0) {
1616 - fullMessage += '<div class="image-gallery" dir="auto">';
847 +
848 + if (images.length > 0) {
849 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
1617 850 images.forEach(img => {
1618 - const safeTitle = sanitizeUserInput(img.title);
1619 - const safeUrl = encodeURI(img.image_url);
1620 - const safeThumbnail = encodeURI(img.thumbnail_url);
1621 -
1622 851 fullMessage += `
1623 852 <div style="margin-bottom: 10px;">
1624 - <strong>${safeTitle}</strong><br>
1625 - <a href="${safeUrl}" target="_blank">
1626 - <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
853 + <strong>${img.title}</strong><br>
854 + <a href="${img.image_url}" target="_blank">
855 + <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1627 856 </a>
1628 857 </div>`;
1629 858 });
1630 859 fullMessage += '</div>';
1631 860 }
1632 -
1633 - // Append HTML content if provided
1634 - if (messageHtml && sender !== "user") {
1635 - // Only add line breaks if there's actual text content before the HTML
1636 - if (fullMessage && fullMessage.trim()) {
1637 - fullMessage += '<br><br>' + messageHtml;
1638 - } else {
1639 - fullMessage = messageHtml;
1640 - }
1641 - }
1642 -
1643 - messageDiv.html(fullMessage);
1644 -
1645 - if (isTemporary) {
1646 - messageDiv.addClass('temporary-message');
1647 - }
1648 -
1649 - // Append to the correct chatbot instance's chat-box
1650 - var $chatBox = getElement(botId, 'chat-box');
1651 - messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1652 - // FIXED: Use event delegation for link tracking
1653 - if (sender === "bot" || sender === "agent") {
1654 - attachLinkTracking(messageDiv, messageText, botId);
1655 - }
1656 -
1657 - if (sender === "bot") {
1658 - const lastUserMessage = $chatBox.find('.user-message').last();
1659 - if (lastUserMessage.length) {
1660 - scrollElementToTop(lastUserMessage, botId);
1661 - }
1662 - }
1663 -
1664 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1665 - mxchatEnsurePrintRoot(botId);
1666 - }
1667 - });
1668 -
1669 - if (messageText.id) {
1670 - var instance = MxChatInstances.get(botId);
1671 - instance.lastSeenMessageId = messageText.id;
1672 - hideNotification(botId);
1673 - }
1674 - } catch (error) {
1675 - // Error rendering message - silently continue
1676 - }
1677 -}
1678 -
1679 -// Helper function to attach link tracking with proper event handling
1680 -function attachLinkTracking(messageDiv, messageText, botId) {
1681 - botId = botId || 'default';
1682 - // Use a slight delay to ensure DOM is ready
1683 - setTimeout(function() {
1684 - const links = messageDiv.find('a[href]').not('[data-tracked]');
1685 -
1686 - links.each(function() {
1687 - const $link = $(this);
1688 - const originalHref = $link.attr('href');
1689 -
1690 - // Mark as tracked to avoid duplicate handlers
1691 - $link.attr('data-tracked', 'true');
1692 -
1693 - // Only track external URLs
1694 - if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1695 - // Remove any existing click handlers first
1696 - $link.off('click.tracking');
1697 -
1698 - // Add new click handler with namespace
1699 - $link.on('click.tracking', function(e) {
1700 - e.preventDefault();
1701 - e.stopPropagation();
1702 -
1703 - const messageContext = typeof messageText === 'string'
1704 - ? messageText.substring(0, 200)
1705 - : '';
1706 -
1707 - // Track the click
1708 - $.ajax({
1709 - url: mxchatChat.ajax_url,
1710 - type: 'POST',
1711 - data: {
1712 - action: 'mxchat_track_url_click',
1713 - session_id: getChatSession(botId),
1714 - url: originalHref,
1715 - message_context: messageContext,
1716 - nonce: mxchatChat.nonce
1717 - },
1718 - complete: function() {
1719 - // Always redirect, even if tracking fails
1720 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1721 - window.open(originalHref, '_blank');
1722 - } else {
1723 - window.location.href = originalHref;
861 +
862 + if (lastMessageDiv.length) {
863 + lastMessageDiv.fadeOut(200, function() {
864 + $(this)
865 + .html(fullMessage)
866 + .removeClass('bot-message user-message')
867 + .addClass(messageClass)
868 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
869 + .css({
870 + 'background-color': bgColor,
871 + 'color': fontColor,
872 + })
873 + .removeClass('temporary-message')
874 + .fadeIn(200, function() {
875 + if (sender === "bot" || sender === "agent") {
876 + const lastUserMessage = $('#chat-box').find('.user-message').last();
877 + if (lastUserMessage.length) {
878 + scrollElementToTop(lastUserMessage);
1724 879 }
880 + // Show notification if chat is hidden
881 + if ($('#floating-chatbot').hasClass('hidden')) {
882 + showNotification();
883 + }
1725 884 }
1726 885 });
1727 -
1728 - return false; // Extra insurance to prevent default
1729 - });
1730 - }
1731 - });
1732 - }, 100); // Small delay to ensure DOM is ready
1733 -}
1734 -
1735 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1736 - var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1737 - var $chatBox = getElement(botId, 'chat-box');
1738 - var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1739 -
1740 - // Determine styles
1741 - let bgColor, fontColor;
1742 - if (sender === "user") {
1743 - bgColor = userMessageBgColor;
1744 - fontColor = userMessageFontColor;
1745 - } else if (sender === "agent") {
1746 - bgColor = liveAgentMessageBgColor;
1747 - fontColor = liveAgentMessageFontColor;
1748 - } else {
1749 - bgColor = botMessageBgColor;
1750 - fontColor = botMessageFontColor;
1751 - }
1752 -
1753 - // Always run linkify to convert markdown links and format text.
1754 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1755 - // to avoid double-processing URLs that are already inside <a> tags).
1756 - var fullMessage = linkify(responseText);
1757 -
1758 - if (responseHtml) {
1759 - // Only add line breaks if there's actual text content before the HTML
1760 - if (fullMessage && fullMessage.trim()) {
1761 - fullMessage += '<br><br>' + responseHtml;
886 + });
1762 887 } else {
1763 - fullMessage = responseHtml;
888 + appendMessage(sender, responseText, responseHtml, images);
1764 889 }
1765 890 }
891 +
892 + function appendThinkingMessage() {
893 + // Remove any existing thinking dots first
894 + $('.thinking-dots').remove();
1766 895
1767 - if (images.length > 0) {
1768 - fullMessage += '<div class="image-gallery" dir="auto">';
1769 - images.forEach(img => {
1770 - fullMessage += `
1771 - <div style="margin-bottom: 10px;">
1772 - <strong>${img.title}</strong><br>
1773 - <a href="${img.image_url}" target="_blank">
1774 - <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1775 - </a>
1776 - </div>`;
1777 - });
1778 - fullMessage += '</div>';
1779 - }
1780 -
1781 - if (lastMessageDiv.length) {
1782 - // Replace content immediately to prevent visual gap between thinking dots and response
1783 - lastMessageDiv
1784 - .html(fullMessage)
1785 - .removeClass('bot-message user-message temporary-message')
1786 - .addClass(messageClass)
1787 - .attr('dir', 'auto');
1788 -
1789 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1790 - var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1791 - if (!skipColors) {
1792 - lastMessageDiv.css({
1793 - 'background-color': bgColor,
1794 - 'color': fontColor,
1795 - });
1796 - }
1797 -
1798 - // Handle link tracking and scroll
1799 - if (sender === "bot" || sender === "agent") {
1800 - attachLinkTracking(lastMessageDiv, responseText, botId);
1801 -
1802 - const lastUserMessage = $chatBox.find('.user-message').last();
1803 - if (lastUserMessage.length) {
1804 - scrollElementToTop(lastUserMessage, botId);
1805 - }
1806 - // Show notification if chat is hidden
1807 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1808 - if ($floatingChatbot.hasClass('hidden')) {
1809 - showNotification(botId);
1810 - }
1811 - }
1812 -
1813 - // Re-enable chat input after response is displayed
1814 - enableChatInput(botId);
1815 -
1816 - if (sender === "bot" || sender === "agent") {
1817 - mxchatEnsurePrintRoot(botId);
1818 - }
1819 - } else {
1820 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1821 - // Re-enable chat input after response is displayed
1822 - enableChatInput(botId);
1823 - }
1824 -}
1825 -
1826 -
1827 - function appendThinkingMessage(botId) {
1828 - botId = botId || 'default';
1829 -
1830 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1831 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1832 - if (indicator && indicator.textContent === 'Live Agent') {
1833 - return;
1834 - }
1835 -
1836 - var $chatBox = getElement(botId, 'chat-box');
1837 -
1838 - // Remove any existing thinking dots in this bot's chat first
1839 - $chatBox.find('.thinking-dots').remove();
1840 -
1841 - // Check if we should skip inline colors (AI theme is active)
1842 - var skipColors = shouldSkipInlineColors(botId);
1843 -
1844 896 // Retrieve the bot message font color and background color
1845 897 var botMessageFontColor = mxchatChat.bot_message_font_color;
1846 898 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1847 899
1848 - // Build thinking dots HTML - skip inline colors if AI theme is active
1849 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
900 +
1850 901 var thinkingHtml = '<div class="thinking-dots-container">' +
1851 902 '<div class="thinking-dots">' +
1852 - '<span class="dot"' + dotStyle + '></span>' +
1853 - '<span class="dot"' + dotStyle + '></span>' +
1854 - '<span class="dot"' + dotStyle + '></span>' +
903 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
904 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
905 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1855 906 '</div>' +
1856 907 '</div>';
1857 908
1858 - // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1859 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1860 - $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1861 - scrollToBottom(botId);
909 + // Append the thinking dots to the chat container (or within the temporary message div)
910 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
911 + scrollToBottom();
1862 912 }
913 +
914 + function removeThinkingDots() {
915 + $('.thinking-dots').closest('.temporary-message').remove();
916 + }
1863 917
1864 - function removeThinkingDots(botId) {
1865 - botId = botId || 'default';
1866 - var $chatBox = getElement(botId, 'chat-box');
1867 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1868 - $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1869 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1870 - }
1871 918
1872 919 // ====================================
1873 920 // TEXT FORMATTING & PROCESSING
1874 921 // ====================================
1875 922
923 +
1876 924 function linkify(inputText) {
1877 - if (!inputText) {
1878 - return '';
1879 - }
925 + if (!inputText) return '';
926 +
927 + // Process markdown headers
928 + let processedText = formatMarkdownHeaders(inputText);
929 +
930 + // Process markdown links
931 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
932 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
933 + const safeUrl = encodeURI(url);
934 + const safeText = sanitizeUserInput(text);
935 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
936 + });
1880 937
1881 - // Helper function to check if URL is already encoded
1882 - function isUrlEncoded(url) {
1883 - // Check for % followed by exactly 2 hex digits
1884 - return /%[0-9a-fA-F]{2}/.test(url);
1885 - }
938 + // Process phone numbers (tel:)
939 + const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
940 + processedText = processedText.replace(phonePattern, (match, text, phone) => {
941 + const safePhone = encodeURI(phone);
942 + const safeText = sanitizeUserInput(text);
943 + return `<a href="${safePhone}">${safeText}</a>`;
944 + });
1886 945
1887 - // Helper function to safely encode URLs only if needed
1888 - function safeEncodeUrl(url) {
1889 - // If URL already contains encoded characters, return as-is
1890 - if (isUrlEncoded(url)) {
1891 - return url;
1892 - }
1893 - // Otherwise, encode it
1894 - return encodeURI(url);
1895 - }
1896 -
1897 - // Process markdown headers FIRST
1898 - let processedText = formatMarkdownHeaders(inputText);
1899 -
1900 - // Process text styling (bold, italic, strikethrough)
1901 - processedText = formatTextStyling(processedText);
1902 -
1903 - // Process code blocks BEFORE processing links
1904 - processedText = formatCodeBlocks(processedText);
1905 -
1906 - // Process markdown tables BEFORE converting newlines to paragraphs
1907 - processedText = formatMarkdownTables(processedText);
1908 -
1909 - // NOW convert to paragraphs
1910 - processedText = convertNewlinesToBreaks(processedText);
1911 -
1912 - // IMPORTANT: Handle citation-style brackets FIRST [URL]
1913 - // This prevents them from being processed as markdown links
1914 - // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1915 - processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1916 - // Clean the URL of any trailing punctuation
1917 - let cleanUrl = url.replace(/[.,;!?]+$/, '');
1918 - const safeUrl = safeEncodeUrl(cleanUrl);
1919 - // Return as a proper link without the brackets
1920 - return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1921 - });
1922 -
1923 - // Process markdown links: [text](url) and [](url)
1924 - // Uses balanced parenthesis matching to handle URLs containing parens
1925 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1926 - processedText = (function(input) {
1927 - var result = '';
1928 - var i = 0;
1929 - while (i < input.length) {
1930 - // Look for [ at current position
1931 - if (input[i] === '[') {
1932 - // Find closing ]
1933 - var closeBracket = input.indexOf(']', i + 1);
1934 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1935 - result += input[i];
1936 - i++;
1937 - continue;
1938 - }
1939 - var linkText = input.substring(i + 1, closeBracket);
1940 - // Check if URL starts with http
1941 - var urlStart = closeBracket + 2;
1942 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1943 - result += input[i];
1944 - i++;
1945 - continue;
1946 - }
1947 - // Find balanced closing paren
1948 - var depth = 1;
1949 - var j = urlStart;
1950 - while (j < input.length && depth > 0) {
1951 - if (input[j] === '(') depth++;
1952 - else if (input[j] === ')') depth--;
1953 - if (depth > 0) j++;
1954 - }
1955 - if (depth !== 0) {
1956 - result += input[i];
1957 - i++;
1958 - continue;
1959 - }
1960 - var url = input.substring(urlStart, j);
1961 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1962 - var encodedUrl = safeEncodeUrl(cleanUrl);
1963 - if (!linkText || !linkText.trim()) {
1964 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1965 - } else {
1966 - var safeText = sanitizeUserInput(linkText);
1967 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1968 - }
1969 - i = j + 1; // Skip past the closing )
1970 - } else {
1971 - result += input[i];
1972 - i++;
1973 - }
1974 - }
1975 - return result;
1976 - })(processedText);
1977 -
1978 - // Process phone numbers: [text](tel:number)
1979 - const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1980 - processedText = processedText.replace(phonePattern, (match, text, phone) => {
1981 - const safePhone = safeEncodeUrl(phone);
1982 - const safeText = sanitizeUserInput(text);
1983 - return `<a href="${safePhone}">${safeText}</a>`;
1984 - });
1985 -
1986 - // Process mailto links: [text](mailto:email)
1987 - const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1988 - processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1989 - const safeMailto = safeEncodeUrl(mailto);
1990 - const safeText = sanitizeUserInput(text);
1991 - return `<a href="${safeMailto}">${safeText}</a>`;
1992 - });
1993 -
1994 - // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1995 - // Updated pattern to be more careful about what it matches
1996 - const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1997 - processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1998 - // Extra check: make sure this isn't already linked
1999 - if (match.includes('href=') || match.includes('</a>')) {
2000 - return match;
2001 - }
946 + // Process standalone URLs
947 + const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
948 + processedText = processedText.replace(urlPattern, (match, prefix, url) => {
949 + const safeUrl = encodeURI(url);
950 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
951 + });
2002 952
2003 - // Clean trailing punctuation
2004 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
2005 - const safeUrl = safeEncodeUrl(cleanUrl);
2006 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2007 - });
2008 -
2009 - // Process www. URLs - but NOT if they're already in <a> tags or brackets
2010 - const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
2011 - processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
2012 - // Extra check: make sure this isn't already linked
2013 - if (match.includes('href=') || match.includes('</a>')) {
2014 - return match;
2015 - }
953 + // Process www. URLs
954 + const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
955 + processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
956 + const safeUrl = encodeURI(`http://${url}`);
957 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
958 + });
2016 959
2017 - // Clean trailing punctuation
2018 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
2019 - const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
2020 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2021 - });
960 + // Add this after your phone pattern
961 + const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
962 + processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
963 + const safeMailto = encodeURI(mailto);
964 + const safeText = sanitizeUserInput(text);
965 + return `<a href="${safeMailto}">${safeText}</a>`;
966 + });
2022 967
2023 - return processedText;
2024 -}
968 + return processedText;
969 + }
2025 970
2026 971 function formatMarkdownHeaders(text) {
2027 972 // Handle h1 to h6 headers
2028 - return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
973 + return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
2029 974 const level = hashes.length;
2030 - return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
975 + return `<h${level} class="chat-heading">${content}</h${level}>`;
2031 976 });
2032 977 }
2033 978
2034 -function formatTextStyling(text) {
2035 - // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
2036 - const protectedSegments = [];
2037 - let protectedText = text;
2038 -
2039 - // Step 1a: Protect HTML href="..." attributes
2040 - protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
2041 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2042 - protectedSegments.push(match);
2043 - return placeholder;
2044 - });
2045 -
2046 - // Step 1b: Protect Markdown links [text](url)
2047 - // This is crucial - we need to protect the URLs in markdown format
2048 - protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
2049 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2050 - protectedSegments.push(match);
2051 - return placeholder;
2052 - });
2053 -
2054 - // Step 1c: Also protect bare URLs that might exist
2055 - protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
2056 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2057 - protectedSegments.push(match);
2058 - return placeholder;
2059 - });
2060 -
2061 - // Step 2: Now apply text styling to the protected text
2062 - // Handle bold text (**text**)
2063 - protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2064 -
2065 - // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
2066 - // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
2067 - protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
2068 -
2069 - // Handle underscores for italic - Safari-compatible (no lookbehind)
2070 - // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
2071 - protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
2072 -
2073 - // Handle strikethrough (~~text~~)
2074 - protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
2075 -
2076 - // Step 3: Restore all protected segments
2077 - protectedSegments.forEach((original, index) => {
2078 - const placeholder = `__PROTECTED_${index}__`;
2079 - protectedText = protectedText.replace(placeholder, original);
2080 - });
2081 -
2082 - return protectedText;
2083 -}
2084 979 function formatBoldText(text) {
2085 - // This function is kept for compatibility but now uses formatTextStyling
2086 - return formatTextStyling(text);
980 + return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2087 981 }
2088 982
2089 -function convertNewlinesToBreaks(text) {
2090 - // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
2091 - const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
983 + function convertNewlinesToBreaks(text) {
984 + // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
985 + const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
986 +
987 + // Wrap each paragraph in <p> tags
988 + return paragraphs
989 + .map(para => `<p>${para.trim()}</p>`)
990 + .join('');
991 + }
2092 992
2093 - // Filter out empty paragraphs and wrap each paragraph in <p> tags
2094 - return paragraphs
2095 - .map(para => para.trim())
2096 - .filter(para => para.length > 0) // Remove empty paragraphs
2097 - .map(para => `<p>${para}</p>`)
2098 - .join('');
2099 -}
2100 993 function formatCodeBlocks(text) {
2101 - // Handle fenced code blocks with language specification (```language)
2102 - text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
2103 - const lang = language || 'text';
2104 - const escapedCode = escapeHtml(code.trim());
2105 - return `<div class="mxchat-code-block-container">
2106 - <div class="mxchat-code-header">
2107 - <span class="mxchat-code-language">${lang}</span>
2108 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
2109 - </div>
2110 - <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
2111 - </div>`;
994 + // First handle raw PHP tags
995 + text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
996 + return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
2112 997 });
2113 -
2114 - // Handle inline code with single backticks
2115 - text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
2116 -
2117 - // Handle raw PHP tags (legacy support)
2118 - text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
2119 - const escapedCode = escapeHtml(match);
2120 - return `<div class="mxchat-code-block-container">
2121 - <div class="mxchat-code-header">
2122 - <span class="mxchat-code-language">php</span>
2123 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
2124 - </div>
2125 - <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
2126 - </div>`;
998 +
999 + // Then handle code blocks with backticks
1000 + text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
1001 + return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
2127 1002 });
2128 -
1003 +
2129 1004 return text;
2130 1005 }
2131 -
2132 - function formatMarkdownTables(text) {
2133 - var lines = text.split('\n');
2134 - var result = [];
2135 - var i = 0;
2136 -
2137 - while (i < lines.length) {
2138 - // Check for a table: current line has pipes AND next line is a separator row
2139 - if (i + 1 < lines.length &&
2140 - lines[i].indexOf('|') !== -1 &&
2141 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2142 -
2143 - var tableLines = [];
2144 - var headerLine = lines[i];
2145 - var separatorLine = lines[i + 1];
2146 - tableLines.push(headerLine);
2147 - tableLines.push(separatorLine);
2148 -
2149 - // Collect remaining table rows
2150 - var j = i + 2;
2151 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2152 - tableLines.push(lines[j]);
2153 - j++;
2154 - }
2155 -
2156 - // Parse alignment from separator row
2157 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2158 - var alignments = sepCells.map(function(cell) {
2159 - var trimmed = cell.trim();
2160 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2161 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2162 - return 'left';
2163 - });
2164 -
2165 - // Build HTML table
2166 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2167 -
2168 - // Header row
2169 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2170 - html += '<thead><tr>';
2171 - headerCells.forEach(function(cell, idx) {
2172 - var align = alignments[idx] || 'left';
2173 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2174 - });
2175 - html += '</tr></thead>';
2176 -
2177 - // Body rows
2178 - html += '<tbody>';
2179 - for (var r = 2; r < tableLines.length; r++) {
2180 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2181 - html += '<tr>';
2182 - rowCells.forEach(function(cell, idx) {
2183 - var align = alignments[idx] || 'left';
2184 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2185 - });
2186 - html += '</tr>';
2187 - }
2188 - html += '</tbody></table></div>';
2189 -
2190 - result.push(html);
2191 - i = j;
2192 - } else {
2193 - result.push(lines[i]);
2194 - i++;
2195 - }
2196 - }
2197 -
2198 - return result.join('\n');
2199 - }
2200 -
1006 +
2201 1007 function sanitizeUserInput(text) {
2202 1008 const div = document.createElement('div');
2203 1009 div.textContent = text;
2204 1010 return div.innerHTML;
@@ -2203,12 +1009,12 @@
2203 1009 div.textContent = text;
2204 1010 return div.innerHTML;
2205 1011 }
2206 1012
1013 +
2207 1014 function escapeHtml(unsafe) {
2208 - // Skip escaping if it's already escaped or contains HTML code block markup
2209 - if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
2210 - unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1015 + // First check if it's already a code block
1016 + if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
2211 1017 return unsafe;
2212 1018 }
2213 1019
2214 1020 return unsafe
@@ -2224,25 +1030,15 @@
2224 1030 textArea.innerHTML = text;
2225 1031 return textArea.value;
2226 1032 }
2227 1033
1034 +
2228 1035 // ====================================
2229 1036 // UI & SCROLLING CONTROLS
2230 1037 // ====================================
2231 1038
2232 - function scrollToBottom(botIdOrInstant, instant) {
2233 - // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
2234 - var botId = 'default';
2235 - if (typeof botIdOrInstant === 'string') {
2236 - botId = botIdOrInstant;
2237 - instant = instant || false;
2238 - } else if (typeof botIdOrInstant === 'boolean') {
2239 - instant = botIdOrInstant;
2240 - } else {
2241 - instant = false;
2242 - }
2243 -
2244 - var chatBox = getElement(botId, 'chat-box');
1039 + function scrollToBottom(instant = false) {
1040 + var chatBox = $('#chat-box');
2245 1041 if (instant) {
2246 1042 // Instantly set the scroll position to the bottom
2247 1043 chatBox.scrollTop(chatBox.prop("scrollHeight"));
2248 1044 } else {
@@ -2251,15 +1047,15 @@
2251 1047 const scrollHeight = chatBox.prop("scrollHeight");
2252 1048 const initialScroll = chatBox.scrollTop();
2253 1049 const distance = scrollHeight - initialScroll;
2254 1050 const duration = 500; // Duration in ms
2255 -
1051 +
2256 1052 function smoothScroll(timestamp) {
2257 1053 if (!start) start = timestamp;
2258 1054 const progress = timestamp - start;
2259 1055 const currentScroll = initialScroll + (distance * (progress / duration));
2260 1056 chatBox.scrollTop(currentScroll);
2261 -
1057 +
2262 1058 if (progress < duration) {
2263 1059 requestAnimationFrame(smoothScroll);
2264 1060 } else {
2265 1061 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -2264,37 +1060,33 @@
2264 1060 } else {
2265 1061 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2266 1062 }
2267 1063 }
2268 -
1064 +
2269 1065 requestAnimationFrame(smoothScroll);
2270 1066 }
2271 1067 }
2272 -
2273 - function scrollElementToTop(element, botId, topOffset) {
2274 - botId = botId || 'default';
2275 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2276 - var chatBox = getElement(botId, 'chat-box');
1068 +
1069 + function scrollElementToTop(element) {
1070 + var chatBox = $('#chat-box');
2277 1071 var elementTop = element.position().top + chatBox.scrollTop();
2278 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1072 + chatBox.animate({ scrollTop: elementTop }, 500);
2279 1073 }
2280 -
2281 - function showChatWidget(botId) {
2282 - botId = botId || 'default';
2283 - var $button = getElement(botId, 'floating-chatbot-button');
1074 +
1075 + function showChatWidget() {
2284 1076 // First ensure display is set
2285 - $button.css('display', 'flex');
1077 + $('#floating-chatbot-button').css('display', 'flex');
2286 1078 // Then handle the fade
2287 - $button.fadeTo(500, 1);
1079 + $('#floating-chatbot-button').fadeTo(500, 1);
2288 1080 // Force visibility
2289 - $button.removeClass('hidden');
1081 + $('#floating-chatbot-button').removeClass('hidden');
1082 + //console.log('Showing widget');
2290 1083 }
2291 -
2292 - function hideChatWidget(botId) {
2293 - botId = botId || 'default';
2294 - var $button = getElement(botId, 'floating-chatbot-button');
2295 - $button.css('display', 'none');
2296 - $button.addClass('hidden');
1084 +
1085 + function hideChatWidget() {
1086 + $('#floating-chatbot-button').css('display', 'none');
1087 + $('#floating-chatbot-button').addClass('hidden');
1088 + //console.log('Hiding widget');
2297 1089 }
2298 1090
2299 1091 function disableScroll() {
2300 1092 if (isMobile()) {
@@ -2323,15 +1115,18 @@
2323 1115 // NOTIFICATION SYSTEM
2324 1116 // ====================================
2325 1117
2326 1118 function createNotificationBadge() {
1119 + //console.log("Creating notification badge...");
2327 1120 const chatButton = document.getElementById('floating-chatbot-button');
2328 -
1121 + //console.log("Chat button found:", !!chatButton);
1122 +
2329 1123 if (!chatButton) return;
2330 -
1124 +
2331 1125 // Remove any existing badge first
2332 1126 const existingBadge = chatButton.querySelector('.chat-notification-badge');
2333 1127 if (existingBadge) {
1128 + //console.log("Removing existing badge");
2334 1129 existingBadge.remove();
2335 1130 }
2336 1131
2337 1132 notificationBadge = document.createElement('div');
@@ -2353,43 +1148,34 @@
2353 1148 chatButton.appendChild(notificationBadge);
2354 1149
2355 1150 }
2356 1151
2357 - function showNotification(botId) {
2358 - botId = botId || 'default';
2359 - const badge = getElementDOM(botId, 'chat-notification-badge');
2360 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2361 - if (badge && $floatingChatbot.hasClass('hidden')) {
1152 + function showNotification() {
1153 + const badge = document.getElementById('chat-notification-badge');
1154 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2362 1155 badge.style.display = 'block';
2363 1156 badge.textContent = '1';
2364 1157 }
2365 1158 }
2366 -
2367 - function hideNotification(botId) {
2368 - botId = botId || 'default';
2369 - const badge = getElementDOM(botId, 'chat-notification-badge');
1159 +
1160 + function hideNotification() {
1161 + const badge = document.getElementById('chat-notification-badge');
2370 1162 if (badge) {
2371 1163 badge.style.display = 'none';
2372 1164 }
2373 1165 }
2374 -
2375 - function startNotificationChecking(botId) {
2376 - botId = botId || 'default';
1166 +
1167 + function startNotificationChecking() {
2377 1168 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2378 1169 if (!chatPersistenceEnabled) return;
2379 -
2380 - createNotificationBadge(botId);
2381 - var instance = MxChatInstances.get(botId);
2382 - instance.notificationCheckInterval = setInterval(function() {
2383 - checkForNewMessages(botId);
2384 - }, 30000); // Check every 30 seconds
1170 +
1171 + createNotificationBadge();
1172 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2385 1173 }
2386 -
2387 - function stopNotificationChecking(botId) {
2388 - botId = botId || 'default';
2389 - var instance = MxChatInstances.get(botId);
2390 - if (instance.notificationCheckInterval) {
2391 - clearInterval(instance.notificationCheckInterval);
1174 +
1175 + function stopNotificationChecking() {
1176 + if (notificationCheckInterval) {
1177 + clearInterval(notificationCheckInterval);
2392 1178 }
2393 1179 }
2394 1180
2395 1181 function checkForNewMessages() {
@@ -2415,35 +1201,50 @@
2415 1201 });
2416 1202 }
2417 1203
2418 1204
2419 -// ====================================
2420 -// LIVE AGENT FUNCTIONALITY
2421 -// ====================================
2422 -
2423 -function startPolling(botId) {
2424 - botId = botId || 'default';
2425 - var instance = MxChatInstances.get(botId);
2426 - // Clear any existing interval first
2427 - stopPolling(botId);
2428 - instance.pollingInterval = setInterval(function() {
2429 - checkForAgentMessages(botId);
2430 - }, 5000);
2431 -}
2432 -
2433 -function stopPolling(botId) {
2434 - botId = botId || 'default';
2435 - var instance = MxChatInstances.get(botId);
2436 - if (instance.pollingInterval) {
2437 - clearInterval(instance.pollingInterval);
2438 - instance.pollingInterval = null;
1205 + // ====================================
1206 + // LIVE AGENT FUNCTIONALITY
1207 + // ====================================
1208 +
1209 + function updateChatModeIndicator(mode) {
1210 + const indicator = document.getElementById('chat-mode-indicator');
1211 + if (indicator) {
1212 + // For Live Agent, keep as is; for AI mode, use the customized text
1213 + if (mode === 'agent') {
1214 + indicator.textContent = 'Live Agent';
1215 + } else {
1216 + // Get the custom AI agent text from a data attribute we'll add to the element
1217 + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1218 + indicator.textContent = customAiText;
1219 + }
1220 + }
1221 + // Start or stop polling based on mode
1222 + if (mode === 'agent') {
1223 + startPolling();
1224 + } else {
1225 + stopPolling();
1226 + }
2439 1227 }
2440 -}
2441 -
2442 -function checkForAgentMessages(botId) {
2443 - botId = botId || 'default';
2444 - var instance = MxChatInstances.get(botId);
2445 - const sessionId = getChatSession(botId);
1228 +
1229 + function startPolling() {
1230 + // Clear any existing interval first
1231 + stopPolling();
1232 + // Start new polling interval
1233 + pollingInterval = setInterval(checkForAgentMessages, 5000);
1234 + //console.log("Started agent message polling");
1235 + }
1236 +
1237 + function stopPolling() {
1238 + if (pollingInterval) {
1239 + clearInterval(pollingInterval);
1240 + pollingInterval = null;
1241 + //console.log("Stopped agent message polling");
1242 + }
1243 + }
1244 +
1245 +function checkForAgentMessages() {
1246 + const sessionId = getChatSession();
2446 1247 $.ajax({
2447 1248 url: mxchatChat.ajax_url,
2448 1249 type: 'POST',
2449 1250 dataType: 'json',
@@ -2449,44 +1250,35 @@
2449 1250 dataType: 'json',
2450 1251 data: {
2451 1252 action: 'mxchat_fetch_new_messages',
2452 1253 session_id: sessionId,
2453 - last_seen_id: instance.lastSeenMessageId,
2454 - persistence_enabled: 'true',
1254 + last_seen_id: lastSeenMessageId,
1255 + persistence_enabled: 'true', // Add this too
2455 1256 nonce: mxchatChat.nonce
2456 1257 },
2457 1258 success: function (response) {
2458 1259 if (response.success && response.data?.new_messages) {
2459 1260 let hasNewMessage = false;
2460 -
1261 +
2461 1262 response.data.new_messages.forEach(function (message) {
2462 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1263 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2463 1264 hasNewMessage = true;
2464 - appendMessage("agent", message.content, '', [], false, botId);
2465 - instance.lastSeenMessageId = message.id;
2466 - instance.processedMessageIds.add(message.id);
1265 + // CHANGE THIS LINE:
1266 + appendMessage("agent", message.content); // Instead of replaceLastMessage
1267 + lastSeenMessageId = message.id;
1268 + processedMessageIds.add(message.id);
2467 1269 }
2468 1270 });
2469 1271
2470 - if (hasNewMessage) {
2471 - enableChatInput(botId);
1272 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1273 + showNotification();
2472 1274 }
2473 -
2474 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2475 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2476 - showNotification(botId);
2477 - }
2478 -
2479 - scrollToBottom(botId, true);
1275 +
1276 + scrollToBottom(true);
2480 1277 }
2481 -
2482 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2483 - if (response.success && response.data?.chat_mode) {
2484 - updateChatModeIndicator(response.data.chat_mode, botId);
2485 - }
2486 1278 },
2487 1279 error: function (xhr, status, error) {
2488 - // Polling error - silently continue
1280 + //console.error("Polling error:", xhr, status, error);
2489 1281 }
2490 1282 });
2491 1283 }
2492 1284
@@ -2492,166 +1284,120 @@
2492 1284
2493 1285 // ====================================
2494 1286 // CHAT HISTORY & PERSISTENCE
2495 1287 // ====================================
2496 -
2497 -function loadChatHistory(botId, onComplete) {
2498 - botId = botId || 'default';
2499 - var instance = MxChatInstances.get(botId);
2500 -
2501 - // Prevent duplicate loading
2502 - if (instance.chatHistoryLoaded) {
2503 - if (onComplete) onComplete();
2504 - return;
2505 - }
2506 -
2507 - // Use getChatSession which returns null if no session exists (does NOT create one)
2508 - var sessionId = getChatSession(botId);
2509 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2510 -
2511 - // No session yet — nothing to load. History will load after first message via ensureSession.
2512 - if (!sessionId) {
2513 - instance.chatHistoryLoaded = true;
2514 - if (onComplete) onComplete();
2515 - return;
2516 - }
2517 -
2518 - if (chatPersistenceEnabled && sessionId) {
2519 - $.ajax({
2520 - url: mxchatChat.ajax_url,
2521 - type: 'POST',
2522 - dataType: 'json',
2523 - data: {
2524 - action: 'mxchat_fetch_conversation_history',
2525 - session_id: sessionId
2526 - },
2527 - success: function(response) {
2528 - // Handle session reset (IP changed while user was away)
2529 - if (response.success === false && response.data && response.data.action === 'reset_session') {
2530 - // Silent reset — new session but don't clear UI
2531 - MxChatInstances.silentResetSession(botId);
2532 - instance.chatHistoryLoaded = true; // Prevent retry loop
2533 - if (onComplete) onComplete();
2534 - return;
2535 - }
2536 -
2537 - // Check if the response indicates success
2538 - if (response.success) {
2539 - // Handle case where conversation data exists and is an array
2540 - if (response.data && Array.isArray(response.data.conversation)) {
2541 - var $chatBox = getElement(botId, 'chat-box');
1288 +
1289 + function loadChatHistory() {
1290 + var sessionId = getChatSession();
1291 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1292 +
1293 + if (chatPersistenceEnabled && sessionId) {
1294 + $.ajax({
1295 + url: mxchatChat.ajax_url,
1296 + type: 'POST',
1297 + dataType: 'json',
1298 + data: {
1299 + action: 'mxchat_fetch_conversation_history',
1300 + session_id: sessionId
1301 + },
1302 + success: function(response) {
1303 + if (response.success && response.data && Array.isArray(response.data.conversation)) {
1304 +
1305 +
1306 + var $chatBox = $('#chat-box');
2542 1307 var $fragment = $(document.createDocumentFragment());
2543 - let highestMessageId = instance.lastSeenMessageId;
2544 -
2545 - // Update chat mode if provided
1308 + let highestMessageId = lastSeenMessageId;
1309 +
2546 1310 if (response.data.chat_mode) {
2547 - updateChatModeIndicator(response.data.chat_mode, botId);
1311 + updateChatModeIndicator(response.data.chat_mode);
2548 1312 }
2549 -
2550 - // Only process if there are actual messages
2551 - if (response.data.conversation.length > 0) {
2552 - // IMPORTANT: Clear existing messages before loading history
2553 - $chatBox.empty();
2554 -
2555 - $.each(response.data.conversation, function(index, message) {
2556 - // Skip agent messages if persistence is off
2557 - if (!chatPersistenceEnabled && message.role === 'agent') {
2558 - return;
2559 - }
2560 -
2561 - var messageClass, messageBgColor, messageFontColor;
2562 -
2563 - switch (message.role) {
2564 - case 'user':
2565 - messageClass = 'user-message';
2566 - messageBgColor = userMessageBgColor;
2567 - messageFontColor = userMessageFontColor;
2568 - break;
2569 - case 'agent':
2570 - messageClass = 'agent-message';
2571 - messageBgColor = liveAgentMessageBgColor;
2572 - messageFontColor = liveAgentMessageFontColor;
2573 - break;
2574 - default:
2575 - messageClass = 'bot-message';
2576 - messageBgColor = botMessageBgColor;
2577 - messageFontColor = botMessageFontColor;
2578 - break;
2579 - }
2580 -
2581 - var messageElement = $('<div>').addClass(messageClass)
2582 - .css({
2583 - 'background': messageBgColor,
2584 - 'color': messageFontColor
2585 - });
2586 -
2587 - var content = message.content;
2588 - content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2589 - content = decodeHTMLEntities(content);
2590 -
2591 - // Skip linkify for messages containing structured HTML
2592 - // (forms, product cards, galleries, etc.) to avoid
2593 - // markdown formatting corrupting HTML attributes
2594 - // (e.g. underscores in name="field_name" becoming <em> tags)
2595 - if (content.includes("mxchat-product-card") ||
2596 - content.includes("mxchat-image-gallery") ||
2597 - content.includes("mxchat-featured-products") ||
2598 - content.includes("<form") ||
2599 - content.includes("<input") ||
2600 - content.includes("<select") ||
2601 - content.includes("<textarea")) {
2602 - messageElement.html(content);
2603 - } else {
2604 - var formattedContent = linkify(content);
2605 - messageElement.html(formattedContent);
2606 - }
2607 -
2608 - $fragment.append(messageElement);
2609 -
2610 - // Track message IDs
1313 +
1314 + $.each(response.data.conversation, function(index, message) {
1315 + // Skip agent messages if persistence is off
1316 + if (!chatPersistenceEnabled && message.role === 'agent') {
1317 + return;
1318 + }
1319 +
1320 + var messageClass, messageBgColor, messageFontColor;
1321 +
1322 + switch (message.role) {
1323 + case 'user':
1324 + messageClass = 'user-message';
1325 + messageBgColor = userMessageBgColor;
1326 + messageFontColor = userMessageFontColor;
1327 + break;
1328 + case 'agent':
1329 + messageClass = 'agent-message';
1330 + messageBgColor = liveAgentMessageBgColor;
1331 + messageFontColor = liveAgentMessageFontColor;
1332 + break;
1333 + default:
1334 + messageClass = 'bot-message';
1335 + messageBgColor = botMessageBgColor;
1336 + messageFontColor = botMessageFontColor;
1337 + break;
1338 + }
1339 +
1340 + var messageElement = $('<div>').addClass(messageClass)
1341 + .css({
1342 + 'background': messageBgColor,
1343 + 'color': messageFontColor
1344 + });
1345 +
1346 + var content = message.content;
1347 + content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1348 + content = decodeHTMLEntities(content);
1349 +
1350 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1351 + messageElement.html(content);
1352 + } else {
1353 + var formattedContent = linkify(
1354 + formatBoldText(
1355 + convertNewlinesToBreaks(formatCodeBlocks(content))
1356 + )
1357 + );
1358 + messageElement.html(formattedContent);
1359 + }
1360 +
1361 + $fragment.append(messageElement);
1362 +
1363 + // In loadChatHistory, change this part:
2611 1364 if (message.id) {
2612 1365 highestMessageId = Math.max(highestMessageId, message.id);
2613 - instance.processedMessageIds.add(message.id);
1366 + processedMessageIds.add(message.id); // Add all message IDs to processed set
2614 1367 }
2615 - });
2616 -
2617 - // Only append messages and scroll if we have content
2618 - $chatBox.append($fragment);
2619 - scrollToBottom(botId, true);
2620 -
2621 - // Collapse quick questions if we have conversation history
2622 - // BUT skip auto-collapse for embedded bots (they should stay expanded)
2623 - if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2624 - collapseQuickQuestions(botId);
1368 + });
1369 +
1370 + $chatBox.append($fragment);
1371 + scrollToBottom(true);
1372 +
1373 + if (response.data.conversation.length > 0 && hasQuickQuestions()) {
1374 + collapseQuickQuestions();
1375 + }
1376 +
1377 + // Update lastSeenMessageId after history loads
1378 + lastSeenMessageId = highestMessageId;
1379 +
1380 + // Only update chat mode if persistence is enabled
1381 + if (chatPersistenceEnabled && response.data.conversation.length > 0) {
1382 + var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1383 + if (lastMessage.role === 'agent') {
1384 + updateChatModeIndicator('agent');
2625 1385 }
2626 -
2627 - // Update lastSeenMessageId after history loads
2628 - instance.lastSeenMessageId = highestMessageId;
2629 -
2630 - // Only update chat mode if persistence is enabled and we have messages
2631 - if (chatPersistenceEnabled) {
2632 - var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2633 - if (lastMessage.role === 'agent') {
2634 - updateChatModeIndicator('agent', botId);
2635 - }
2636 - }
2637 -
2638 - // Mark as loaded ONLY after successful load
2639 - instance.chatHistoryLoaded = true;
2640 1386 }
1387 + } else {
1388 + console.warn("No conversation history found.");
2641 1389 }
1390 + },
1391 + error: function(xhr, status, error) {
1392 + //console.error("Error loading chat history:", status, error);
1393 + appendMessage("bot", "Unable to load chat history.");
2642 1394 }
2643 - if (onComplete) onComplete();
2644 - },
2645 - error: function(xhr, status, error) {
2646 - // Error loading chat history - silently continue
2647 - if (onComplete) onComplete();
2648 - }
2649 - });
2650 - } else {
2651 - if (onComplete) onComplete();
1395 + });
1396 + } else {
1397 + console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
1398 + }
2652 1399 }
2653 -}
2654 1400
2655 1401
2656 1402 // ====================================
2657 1403 // FILE UPLOAD FUNCTIONALITY
@@ -2663,42 +1409,40 @@
2663 1409 element.addEventListener(eventType, handler);
2664 1410 }
2665 1411 }
2666 1412
2667 - function showActivePdf(filename, botId) {
2668 - botId = botId || 'default';
2669 - const container = getElementDOM(botId, 'active-pdf-container');
2670 - const nameElement = getElementDOM(botId, 'active-pdf-name');
2671 -
1413 + function showActivePdf(filename) {
1414 + const container = document.getElementById('active-pdf-container');
1415 + const nameElement = document.getElementById('active-pdf-name');
1416 +
2672 1417 if (!container || !nameElement) {
1418 + //console.error('PDF container elements not found');
2673 1419 return;
2674 1420 }
2675 -
1421 +
2676 1422 nameElement.textContent = filename;
2677 1423 container.style.display = 'flex';
2678 1424 }
2679 -
2680 - function showActiveWord(filename, botId) {
2681 - botId = botId || 'default';
2682 - const container = getElementDOM(botId, 'active-word-container');
2683 - const nameElement = getElementDOM(botId, 'active-word-name');
2684 -
1425 +
1426 + function showActiveWord(filename) {
1427 + const container = document.getElementById('active-word-container');
1428 + const nameElement = document.getElementById('active-word-name');
1429 +
2685 1430 if (!container || !nameElement) {
1431 + //console.error('Word document container elements not found');
2686 1432 return;
2687 1433 }
2688 -
1434 +
2689 1435 nameElement.textContent = filename;
2690 1436 container.style.display = 'flex';
2691 1437 }
2692 -
2693 - function removeActivePdf(botId) {
2694 - botId = botId || 'default';
2695 - var instance = MxChatInstances.get(botId);
2696 - const container = getElementDOM(botId, 'active-pdf-container');
2697 - const nameElement = getElementDOM(botId, 'active-pdf-name');
2698 -
2699 - if (!container || !nameElement || !instance.activePdfFile) return;
2700 -
1438 +
1439 + function removeActivePdf() {
1440 + const container = document.getElementById('active-pdf-container');
1441 + const nameElement = document.getElementById('active-pdf-name');
1442 +
1443 + if (!container || !nameElement || !activePdfFile) return;
1444 +
2701 1445 fetch(mxchatChat.ajax_url, {
2702 1446 method: 'POST',
2703 1447 headers: {
2704 1448 'Content-Type': 'application/x-www-form-urlencoded',
@@ -2704,9 +1448,9 @@
2704 1448 'Content-Type': 'application/x-www-form-urlencoded',
2705 1449 },
2706 1450 body: new URLSearchParams({
2707 1451 'action': 'mxchat_remove_pdf',
2708 - 'session_id': getChatSession(botId),
1452 + 'session_id': sessionId,
2709 1453 'nonce': mxchatChat.nonce
2710 1454 })
2711 1455 })
2712 1456 .then(response => response.json())
@@ -2718,12 +1462,12 @@
2718 1462 appendMessage('bot', 'PDF removed.');
2719 1463 }
2720 1464 })
2721 1465 .catch(error => {
2722 - // Error removing PDF - silently continue
1466 + //console.error('Error removing PDF:', error);
2723 1467 });
2724 1468 }
2725 -
1469 +
2726 1470 function removeActiveWord() {
2727 1471 const container = document.getElementById('active-word-container');
2728 1472 const nameElement = document.getElementById('active-word-name');
2729 1473
@@ -2749,53 +1493,56 @@
2749 1493 appendMessage('bot', 'Word document removed.');
2750 1494 }
2751 1495 })
2752 1496 .catch(error => {
2753 - // Error removing Word document - silently continue
1497 + //console.error('Error removing Word document:', error);
2754 1498 });
2755 1499 }
2756 -
1500 +
2757 1501 // ====================================
2758 1502 // CONSENT & COMPLIANCE (GDPR)
2759 1503 // ====================================
2760 -
2761 - function initializeChatVisibility(botId) {
2762 - botId = botId || 'default';
2763 - const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2764 - mxchatChat.complianz_toggle === '1' ||
1504 +
1505 + function initializeChatVisibility() {
1506 + //console.log('Initializing chat visibility');
1507 + const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1508 + mxchatChat.complianz_toggle === '1' ||
2765 1509 mxchatChat.complianz_toggle === 1;
2766 -
1510 +
2767 1511 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2768 1512 // Initial check
2769 - checkConsentAndShowChat(botId);
2770 -
1513 + checkConsentAndShowChat();
1514 +
2771 1515 // Listen for consent changes
2772 1516 $(document).on('cmplz_status_change', function(event) {
2773 - checkConsentAndShowChat(botId);
1517 + //console.log('Status change detected');
1518 + checkConsentAndShowChat();
2774 1519 });
2775 1520 } else {
2776 1521 // If Complianz is not enabled, always show
2777 - getElement(botId, 'floating-chatbot-button')
1522 + $('#floating-chatbot-button')
2778 1523 .css('display', 'flex')
2779 1524 .removeClass('hidden no-consent')
2780 1525 .fadeTo(500, 1);
2781 -
1526 +
2782 1527 // Also check pre-chat message when Complianz is not enabled
2783 - checkPreChatDismissal(botId);
1528 + checkPreChatDismissal();
2784 1529 }
2785 1530 }
2786 1531
2787 -
2788 - function checkConsentAndShowChat(botId) {
2789 - botId = botId || 'default';
1532 +
1533 + function checkConsentAndShowChat() {
2790 1534 var consentStatus = cmplz_has_consent('marketing');
2791 1535 var consentType = complianz.consenttype;
2792 -
2793 - let $widget = getElement(botId, 'floating-chatbot-button');
2794 - let $chatbot = getElement(botId, 'floating-chatbot');
2795 - let $preChat = getElement(botId, 'pre-chat-message');
2796 -
1536 +
1537 + //console.log('Checking consent:', {status: consentStatus,type: consentType});
1538 +
1539 + let $widget = $('#floating-chatbot-button');
1540 + let $chatbot = $('#floating-chatbot');
1541 + let $preChat = $('#pre-chat-message');
1542 +
2797 1543 if (consentStatus === true) {
1544 + //console.log('Consent granted - showing widget');
2798 1545 $widget
2799 1546 .removeClass('no-consent')
2800 1547 .css('display', 'flex')
2801 1548 .removeClass('hidden')
@@ -2800,12 +1547,13 @@
2800 1547 .css('display', 'flex')
2801 1548 .removeClass('hidden')
2802 1549 .fadeTo(500, 1);
2803 1550 $chatbot.removeClass('no-consent');
2804 -
1551 +
2805 1552 // Show pre-chat message if not dismissed
2806 - checkPreChatDismissal(botId);
1553 + checkPreChatDismissal();
2807 1554 } else {
1555 + //console.log('No consent - hiding widget');
2808 1556 $widget
2809 1557 .addClass('no-consent')
2810 1558 .fadeTo(500, 0, function() {
2811 1559 $(this)
@@ -2812,9 +1560,9 @@
2812 1560 .css('display', 'none')
2813 1561 .addClass('hidden');
2814 1562 });
2815 1563 $chatbot.addClass('no-consent');
2816 -
1564 +
2817 1565 // Hide pre-chat message when no consent
2818 1566 $preChat.hide();
2819 1567 }
2820 1568 }
@@ -2822,38 +1570,46 @@
2822 1570
2823 1571 // ====================================
2824 1572 // PRE-CHAT MESSAGE HANDLING
2825 1573 // ====================================
2826 -
2827 - function checkPreChatDismissal(botId) {
2828 - botId = botId || 'default';
2829 - try {
2830 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2831 - if (dismissedAt) {
2832 - // Re-show after 24 hours
2833 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2834 - if (elapsed < 86400000) {
2835 - getElement(botId, 'pre-chat-message').hide();
2836 - return;
1574 +
1575 + function checkPreChatDismissal() {
1576 + $.ajax({
1577 + url: mxchatChat.ajax_url,
1578 + type: 'POST',
1579 + data: {
1580 + action: 'mxchat_check_pre_chat_message_status',
1581 + _ajax_nonce: mxchatChat.nonce
1582 + },
1583 + success: function(response) {
1584 + if (response.success && !response.data.dismissed) {
1585 + $('#pre-chat-message').fadeIn(250);
1586 + } else {
1587 + $('#pre-chat-message').hide();
2837 1588 }
2838 - // Expired — clear and show again
2839 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
1589 + },
1590 + error: function() {
1591 + //console.error('Failed to check pre-chat message dismissal status.');
2840 1592 }
2841 - getElement(botId, 'pre-chat-message').fadeIn(250);
2842 - } catch (e) {
2843 - // localStorage unavailable — show the message
2844 - getElement(botId, 'pre-chat-message').fadeIn(250);
2845 - }
1593 + });
2846 1594 }
2847 -
2848 - function handlePreChatDismissal(botId) {
2849 - botId = botId || 'default';
2850 - getElement(botId, 'pre-chat-message').fadeOut(200);
2851 - try {
2852 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2853 - } catch (e) {
2854 - // localStorage unavailable — dismissal won't persist
2855 - }
1595 +
1596 + function handlePreChatDismissal() {
1597 + $('#pre-chat-message').fadeOut(200);
1598 + $.ajax({
1599 + url: mxchatChat.ajax_url,
1600 + type: 'POST',
1601 + data: {
1602 + action: 'mxchat_dismiss_pre_chat_message',
1603 + _ajax_nonce: mxchatChat.nonce
1604 + },
1605 + success: function() {
1606 + $('#pre-chat-message').hide();
1607 + },
1608 + error: function() {
1609 + //console.error('Failed to dismiss pre-chat message.');
1610 + }
1611 + });
2856 1612 }
2857 1613
2858 1614
2859 1615 // ====================================
@@ -2879,104 +1635,89 @@
2879 1635 // ====================================
2880 1636
2881 1637 $(document).on('click', '.mxchat-popular-question', function () {
2882 1638 var question = $(this).text();
2883 - var botId = getBotIdFromElement(this);
2884 -
1639 +
2885 1640 // Append the question as if the user typed it
2886 - appendMessage("user", question, '', [], false, botId);
2887 -
1641 + appendMessage("user", question);
1642 +
2888 1643 // Only collapse if there are questions
2889 - if (hasQuickQuestions(botId)) {
2890 - collapseQuickQuestions(botId);
1644 + if (hasQuickQuestions()) {
1645 + collapseQuickQuestions();
2891 1646 }
2892 -
1647 +
2893 1648 // Send the question to the server
2894 - sendMessageToChatbot(question, botId);
1649 + sendMessageToChatbot(question);
2895 1650 });
2896 1651
2897 1652 $(document).on('click', '.questions-toggle-btn', function(e) {
2898 1653 e.preventDefault();
2899 1654 e.stopPropagation();
2900 - var botId = getBotIdFromElement(this);
2901 - expandQuickQuestions(botId);
1655 + expandQuickQuestions();
2902 1656 });
2903 1657
2904 1658 $(document).on('click', '.questions-collapse-btn', function(e) {
2905 1659 e.preventDefault();
2906 1660 e.stopPropagation();
2907 - var botId = getBotIdFromElement(this);
2908 - collapseQuickQuestions(botId);
1661 + collapseQuickQuestions();
2909 1662 });
2910 1663
2911 - // Chatbot visibility toggle handlers - use class selector for multi-instance support
2912 - $(document).on('click', '.floating-chatbot-button', function() {
2913 - var botId = getBotIdFromElement(this);
2914 - var $chatbot = getElement(botId, 'floating-chatbot');
2915 - var $badge = getElement(botId, 'chat-notification-badge');
2916 - var $preChat = getElement(botId, 'pre-chat-message');
2917 -
2918 - if ($chatbot.hasClass('hidden')) {
2919 - $chatbot.removeClass('hidden').addClass('visible');
1664 + // Chatbot visibility toggle handlers
1665 + $(document).on('click', '#floating-chatbot-button', function() {
1666 + var chatbot = $('#floating-chatbot');
1667 + if (chatbot.hasClass('hidden')) {
1668 + chatbot.removeClass('hidden').addClass('visible');
2920 1669 $(this).addClass('hidden');
2921 - $badge.hide(); // Hide notification when opening chat
1670 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
2922 1671 disableScroll();
2923 - $preChat.fadeOut(250);
2924 -
2925 - // Load chat history for returning visitors (persistence)
2926 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2927 - if (chatPersistenceEnabled) {
2928 - MxChatInstances.ensureSession(botId);
2929 - }
2930 -
2931 - // Deferred email check — only on first widget open
2932 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2933 - var instance = MxChatInstances.get(botId);
2934 - if (emailBlocker && !instance.emailCheckDone) {
2935 - instance.emailCheckDone = true;
2936 - resolveEmailState(botId);
2937 - } else if (!emailBlocker) {
2938 - // No email collection — still route through showChatContainerForBot
2939 - // so the loader is shown while chat history loads
2940 - showChatContainerForBot(botId);
2941 - }
1672 + $('#pre-chat-message').fadeOut(250);
2942 1673 } else {
2943 - $chatbot.removeClass('visible').addClass('hidden');
1674 + chatbot.removeClass('visible').addClass('hidden');
2944 1675 $(this).removeClass('hidden');
2945 1676 enableScroll();
2946 - checkPreChatDismissal(botId);
1677 + checkPreChatDismissal();
2947 1678 }
2948 1679 });
2949 -
2950 - // Allow clicking anywhere on the title bar to close the chatbot
2951 - $(document).on('click', '.chatbot-top-bar', function() {
2952 - var botId = getBotIdFromElement(this);
2953 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2954 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
1680 +
1681 + $(document).on('click', '#exit-chat-button', function() {
1682 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
1683 + $('#floating-chatbot-button').removeClass('hidden');
2955 1684 enableScroll();
2956 1685 });
2957 -
1686 +
2958 1687 $(document).on('click', '.close-pre-chat-message', function(e) {
2959 1688 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2960 - var botId = getBotIdFromElement(this);
2961 - handlePreChatDismissal(botId);
1689 + $('#pre-chat-message').fadeOut(200, function() {
1690 + $(this).remove();
1691 + });
2962 1692 });
2963 -
2964 -
2965 - // PDF upload button handlers - use class selector
2966 - $(document).on('click', '.pdf-upload-btn', function() {
2967 - var botId = getBotIdFromElement(this);
2968 - var pdfInput = getElementDOM(botId, 'pdf-upload');
2969 - if (pdfInput) pdfInput.click();
1693 +
1694 + // Add to Cart button handler
1695 + $(document).on('click', '.mxchat-add-to-cart-button', function() {
1696 + var productId = $(this).data('product-id');
1697 +
1698 + // Get the button text instead of hardcoded "add to cart"
1699 + var buttonText = $(this).text() || "add to cart";
1700 +
1701 + // Add a special prefix to indicate this is from button
1702 + appendMessage("user", buttonText);
1703 + sendMessageToChatbot("!addtocart"); // Special command to indicate button click
2970 1704 });
2971 1705
2972 - // Word upload button handlers - use class selector
2973 - $(document).on('click', '.word-upload-btn', function() {
2974 - var botId = getBotIdFromElement(this);
2975 - var wordInput = getElementDOM(botId, 'word-upload');
2976 - if (wordInput) wordInput.click();
2977 - });
1706 + // PDF upload button handlers
1707 + if (document.getElementById('pdf-upload-btn')) {
1708 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1709 + document.getElementById('pdf-upload').click();
1710 + });
1711 + }
2978 1712
1713 + // Word upload button handlers
1714 + if (document.getElementById('word-upload-btn')) {
1715 + document.getElementById('word-upload-btn').addEventListener('click', function() {
1716 + document.getElementById('word-upload').click();
1717 + });
1718 + }
1719 +
2979 1720 // PDF file input change handler
2980 1721 addSafeEventListener('pdf-upload', 'change', async function(e) {
2981 1722 const file = e.target.files[0];
2982 1723
@@ -2985,13 +1726,15 @@
2985 1726 return;
2986 1727 }
2987 1728
2988 1729 if (!sessionId) {
1730 + //console.error('No session ID found');
2989 1731 alert('Error: No session ID found');
2990 1732 return;
2991 1733 }
2992 -
1734 +
2993 1735 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1736 + //console.error('mxchatChat not properly configured:', mxchatChat);
2994 1737 alert('Error: Ajax configuration missing');
2995 1738 return;
2996 1739 }
2997 1740
@@ -3033,11 +1776,13 @@
3033 1776 appendMessage('bot', data.data.message);
3034 1777 scrollToBottom();
3035 1778 activePdfFile = data.data.filename;
3036 1779 } else {
1780 + //console.error('Upload failed:', data.data);
3037 1781 alert('Failed to upload PDF. Please try again.');
3038 1782 }
3039 1783 } catch (error) {
1784 + //console.error('Upload error:', error);
3040 1785 alert('Error uploading file. Please try again.');
3041 1786 } finally {
3042 1787 uploadBtn.disabled = false;
3043 1788 sendBtn.disabled = false;
@@ -3055,12 +1800,13 @@
3055 1800 return;
3056 1801 }
3057 1802
3058 1803 if (!sessionId) {
1804 + //console.error('No session ID found');
3059 1805 alert('Error: No session ID found');
3060 1806 return;
3061 1807 }
3062 -
1808 +
3063 1809 // Disable buttons and show loading state
3064 1810 const uploadBtn = document.getElementById('word-upload-btn');
3065 1811 const sendBtn = document.getElementById('send-button');
3066 1812 const originalBtnContent = uploadBtn.innerHTML;
@@ -3098,11 +1844,13 @@
3098 1844 appendMessage('bot', data.data.message);
3099 1845 scrollToBottom();
3100 1846 activeWordFile = data.data.filename;
3101 1847 } else {
1848 + //console.error('Upload failed:', data.data);
3102 1849 alert('Failed to upload Word document. Please try again.');
3103 1850 }
3104 1851 } catch (error) {
1852 + //console.error('Upload error:', error);
3105 1853 alert('Error uploading file. Please try again.');
3106 1854 } finally {
3107 1855 uploadBtn.disabled = false;
3108 1856 sendBtn.disabled = false;
@@ -3160,242 +1908,28 @@
3160 1908 });
3161 1909
3162 1910
3163 1911 // ====================================
3164 -// INIT LOADER & CHAT CONTAINER HELPERS
1912 +// IMPROVED EMAIL COLLECTION SETUP
3165 1913 // ====================================
3166 -// These must be outside the email collection block so they're always available
3167 -// (used by persistence loading even when email collection is off)
3168 1914
3169 -function showInitLoader(botId) {
3170 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3171 - if (loader) loader.style.display = 'flex';
3172 -}
1915 +// Email collection form setup and handlers
1916 +const emailForm = document.getElementById('email-collection-form');
1917 +const emailBlocker = document.getElementById('email-blocker');
1918 +const chatbotWrapper = document.getElementById('chat-container');
3173 1919
3174 -function hideInitLoader(botId) {
3175 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3176 - if (loader) loader.style.display = 'none';
3177 -}
3178 -
3179 -function showEmailFormForBot(botId) {
3180 - hideInitLoader(botId);
3181 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3182 - var chatContainer = getElementDOM(botId, 'chat-container');
3183 - if (emailBlocker) emailBlocker.style.display = 'flex';
3184 - if (chatContainer) chatContainer.style.display = 'none';
3185 -}
3186 -
3187 -function showChatContainerForBot(botId) {
3188 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3189 - var chatContainer = getElementDOM(botId, 'chat-container');
3190 - if (emailBlocker) emailBlocker.style.display = 'none';
3191 -
3192 - var instance = MxChatInstances.get(botId);
3193 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3194 -
3195 - // If persistence is on and history hasn't loaded yet, show loader
3196 - // while history loads to prevent flash of empty chat
3197 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3198 - if (chatContainer) chatContainer.style.display = 'none';
3199 - showInitLoader(botId);
3200 - loadChatHistory(botId, function() {
3201 - hideInitLoader(botId);
3202 - if (chatContainer) chatContainer.style.display = 'flex';
3203 - scrollToBottom(botId, true);
3204 - });
3205 - } else {
3206 - hideInitLoader(botId);
3207 - if (chatContainer) chatContainer.style.display = 'flex';
3208 - if (typeof loadChatHistory === 'function') {
3209 - loadChatHistory(botId);
3210 - }
3211 - }
3212 -}
3213 -
3214 -// ====================================
3215 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3216 -// ====================================
3217 -// Only run email collection setup if it's enabled
3218 -if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
3219 -
3220 - // Track submitting state per bot
3221 - const emailSubmittingState = {};
3222 -
3223 - // Add CSS animations for email form (once globally)
3224 - if (!document.getElementById('email-error-styles')) {
3225 - const style = document.createElement('style');
3226 - style.id = 'email-error-styles';
3227 - style.textContent = `
3228 - @keyframes fadeInError {
3229 - from { opacity: 0; transform: translateY(-5px); }
3230 - to { opacity: 1; transform: translateY(0); }
3231 - }
3232 - .email-input-shake {
3233 - animation: shake 0.5s ease-in-out;
3234 - }
3235 - @keyframes shake {
3236 - 0%, 100% { transform: translateX(0); }
3237 - 25% { transform: translateX(-5px); }
3238 - 75% { transform: translateX(5px); }
3239 - }
3240 - @keyframes spin {
3241 - from { transform: rotate(0deg); }
3242 - to { transform: rotate(360deg); }
3243 - }
3244 - .email-spinner {
3245 - display: inline-block;
3246 - vertical-align: middle;
3247 - }
3248 - `;
3249 - document.head.appendChild(style);
3250 - }
3251 -
3252 - function isValidEmailAddress(email) {
3253 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3254 - return emailRegex.test(email.trim()) && email.length <= 254;
3255 - }
3256 -
3257 - function isValidNameInput(name) {
3258 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3259 - }
3260 -
3261 - /**
3262 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3263 - * @param {string} botId - The bot instance ID
3264 - * @param {string} visitorName - The visitor's name to insert
3265 - */
3266 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3267 - var chatBox = getElementDOM(botId, 'chat-box');
3268 - if (!chatBox) return;
3269 -
3270 - // Find the first bot message (intro message)
3271 - var introMessage = chatBox.querySelector('.bot-message');
3272 - if (!introMessage) return;
3273 -
3274 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3275 - if (!messageContent) return;
3276 -
3277 - var html = messageContent.innerHTML;
3278 -
3279 - // Replace {visitor_name} placeholder (case-insensitive)
3280 - if (visitorName && visitorName.trim()) {
3281 - // Escape HTML to prevent XSS
3282 - var safeName = $('<div>').text(visitorName.trim()).html();
3283 - html = html.replace(/\{visitor_name\}/gi, safeName);
3284 - } else {
3285 - // Remove placeholder and clean up spacing if no name provided
3286 - html = html.replace(/\{visitor_name\}/gi, '');
3287 - // Clean up any double spaces that might result
3288 - html = html.replace(/\s{2,}/g, ' ').trim();
3289 - }
3290 -
3291 - messageContent.innerHTML = html;
3292 - }
3293 -
3294 - function setEmailSubmissionState(botId, loading) {
3295 - var submitButton = getElementDOM(botId, 'email-submit-button');
3296 - var emailInput = getElementDOM(botId, 'user-email');
3297 - var nameInput = getElementDOM(botId, 'user-name');
3298 -
3299 - if (loading) {
3300 - emailSubmittingState[botId] = true;
3301 - if (submitButton) submitButton.disabled = true;
3302 - if (emailInput) emailInput.disabled = true;
3303 - if (nameInput) nameInput.disabled = true;
3304 -
3305 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3306 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3307 - const originalText = submitButton.textContent;
3308 - submitButton.innerHTML = `
3309 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3310 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3311 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3312 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3313 - </circle>
3314 - </svg>
3315 - ${originalText}
3316 - `;
3317 - submitButton.style.opacity = '0.8';
3318 - }
3319 - } else {
3320 - emailSubmittingState[botId] = false;
3321 - if (submitButton) submitButton.disabled = false;
3322 - if (emailInput) emailInput.disabled = false;
3323 - if (nameInput) nameInput.disabled = false;
3324 -
3325 - if (submitButton) {
3326 - const originalHtml = submitButton.getAttribute('data-original-html');
3327 - if (originalHtml) {
3328 - submitButton.innerHTML = originalHtml;
3329 - }
3330 - submitButton.style.opacity = '1';
3331 - }
3332 - }
3333 - }
3334 -
3335 - function showEmailError(botId, message) {
3336 - clearEmailError(botId);
3337 -
3338 - var emailForm = getElementDOM(botId, 'email-collection-form');
3339 - if (!emailForm) return;
3340 -
3341 - const errorDiv = document.createElement('div');
3342 - errorDiv.className = 'email-error';
3343 - errorDiv.style.cssText = `
3344 - color: #e74c3c;
3345 - font-size: 12px;
3346 - margin-top: 8px;
3347 - padding: 4px 0;
3348 - animation: fadeInError 0.3s ease;
3349 - `;
3350 - errorDiv.textContent = message;
3351 - emailForm.appendChild(errorDiv);
3352 -
3353 - // Add shake animation to inputs
3354 - var emailInput = getElementDOM(botId, 'user-email');
3355 - var nameInput = getElementDOM(botId, 'user-name');
3356 -
3357 - if (emailInput) {
3358 - emailInput.classList.add('email-input-shake');
3359 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3360 - }
3361 - if (nameInput) {
3362 - nameInput.classList.add('email-input-shake');
3363 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3364 - }
3365 - }
3366 -
3367 - function clearEmailError(botId) {
3368 - var emailForm = getElementDOM(botId, 'email-collection-form');
3369 - if (emailForm) {
3370 - const existingErrors = emailForm.querySelectorAll('.email-error');
3371 - existingErrors.forEach(error => error.remove());
3372 - }
3373 - }
3374 -
3375 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3376 - function resolveEmailState(botId) {
3377 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3378 - if (mxchatChat.initial_email_state.show_email_form) {
3379 - showEmailFormForBot(botId);
3380 - } else {
3381 - showChatContainerForBot(botId);
3382 - }
3383 - } else {
3384 - checkSessionAndEmailForBot(botId);
3385 - }
3386 - }
3387 -
3388 - function checkSessionAndEmailForBot(botId) {
3389 - const sessionId = MxChatInstances.ensureSession(botId);
3390 -
3391 - // Hide both panels while we check — show loader instead
3392 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3393 - var chatContainer = getElementDOM(botId, 'chat-container');
3394 - if (emailBlocker) emailBlocker.style.display = 'none';
3395 - if (chatContainer) chatContainer.style.display = 'none';
3396 - showInitLoader(botId);
3397 -
1920 +if (emailForm && emailBlocker && chatbotWrapper) {
1921 + // Add loading state management
1922 + let isSubmitting = false;
1923 +
1924 + // Check if email exists for the current session
1925 + function checkSessionAndEmail() {
1926 + const sessionId = getChatSession();
1927 +
1928 + // Add timeout to prevent hanging
1929 + const controller = new AbortController();
1930 + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
1931 +
3398 1932 fetch(mxchatChat.ajax_url, {
3399 1933 method: 'POST',
3400 1934 headers: {
3401 1935 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3403,11 +1937,13 @@
3403 1937 body: new URLSearchParams({
3404 1938 action: 'mxchat_check_email_provided',
3405 1939 session_id: sessionId,
3406 1940 nonce: mxchatChat.nonce,
3407 - })
1941 + }),
1942 + signal: controller.signal
3408 1943 })
3409 1944 .then((response) => {
1945 + clearTimeout(timeoutId);
3410 1946 if (!response.ok) {
3411 1947 throw new Error(`HTTP error! status: ${response.status}`);
3412 1948 }
3413 1949 return response.json();
@@ -3414,79 +1950,157 @@
3414 1950 })
3415 1951 .then((data) => {
3416 1952 if (data.success) {
3417 1953 if (data.data.logged_in || data.data.email) {
3418 - showChatContainerForBot(botId);
1954 + showChatContainer();
3419 1955 } else {
3420 - showEmailFormForBot(botId);
1956 + showEmailForm();
3421 1957 }
3422 1958 } else {
3423 - showEmailFormForBot(botId);
1959 + // On error, default to showing email form
1960 + showEmailForm();
3424 1961 }
3425 1962 })
3426 1963 .catch((error) => {
3427 - showEmailFormForBot(botId);
1964 + clearTimeout(timeoutId);
1965 + console.warn('Email check failed, defaulting to email form:', error);
1966 + showEmailForm();
3428 1967 });
3429 1968 }
3430 1969
3431 - // Event delegation for email form submission
3432 - $(document).on('submit', '.email-collection-form', function(e) {
3433 - e.preventDefault();
3434 - e.stopPropagation();
1970 + // Optimized UI transition functions
1971 + function showEmailForm() {
1972 + emailBlocker.style.display = 'flex';
1973 + chatbotWrapper.style.display = 'none';
1974 + }
3435 1975
3436 - var botId = getBotIdFromElement(this);
1976 + function showChatContainer() {
1977 + // Show chat immediately without delay
1978 + emailBlocker.style.display = 'none';
1979 + chatbotWrapper.style.display = 'flex';
1980 +
1981 + // Load chat history only after showing chat container
1982 + if (typeof loadChatHistory === 'function') {
1983 + loadChatHistory();
1984 + }
1985 + }
3437 1986
1987 + // Enhanced email validation
1988 + function isValidEmail(email) {
1989 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1990 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
1991 + }
1992 +
1993 + // Show loading state with spinner
1994 + function setSubmissionState(loading) {
1995 + const submitButton = document.getElementById('email-submit-button');
1996 + const emailInput = document.getElementById('user-email');
1997 +
1998 + if (loading) {
1999 + isSubmitting = true;
2000 + submitButton.disabled = true;
2001 + emailInput.disabled = true;
2002 +
2003 + // Store original content and add spinner
2004 + if (!submitButton.getAttribute('data-original-html')) {
2005 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2006 + }
2007 +
2008 + // Add loading spinner while keeping original text
2009 + const originalText = submitButton.textContent;
2010 + submitButton.innerHTML = `
2011 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2012 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2013 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2014 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2015 + </circle>
2016 + </svg>
2017 + ${originalText}
2018 + `;
2019 +
2020 + submitButton.style.opacity = '0.8';
2021 + } else {
2022 + isSubmitting = false;
2023 + submitButton.disabled = false;
2024 + emailInput.disabled = false;
2025 +
2026 + // Restore original content
2027 + const originalHtml = submitButton.getAttribute('data-original-html');
2028 + if (originalHtml) {
2029 + submitButton.innerHTML = originalHtml;
2030 + }
2031 +
2032 + submitButton.style.opacity = '1';
2033 + }
2034 + }
2035 +
2036 + // Add CSS for spinner animation if not already present
2037 + if (!document.getElementById('email-spinner-styles')) {
2038 + const style = document.createElement('style');
2039 + style.id = 'email-spinner-styles';
2040 + style.textContent = `
2041 + @keyframes spin {
2042 + from { transform: rotate(0deg); }
2043 + to { transform: rotate(360deg); }
2044 + }
2045 + .email-spinner {
2046 + display: inline-block;
2047 + vertical-align: middle;
2048 + }
2049 + `;
2050 + document.head.appendChild(style);
2051 + }
2052 +
2053 + // Handle email form submission with improved error handling
2054 + emailForm.addEventListener('submit', function (event) {
2055 + event.preventDefault();
2056 +
3438 2057 // Prevent double submission
3439 - if (emailSubmittingState[botId]) {
3440 - return false;
2058 + if (isSubmitting) {
2059 + return;
3441 2060 }
3442 2061
3443 - var emailInput = getElementDOM(botId, 'user-email');
3444 - var nameInput = getElementDOM(botId, 'user-name');
3445 - var userEmail = emailInput ? emailInput.value.trim() : '';
3446 - var userName = nameInput ? nameInput.value.trim() : '';
3447 - var sessionId = MxChatInstances.ensureSession(botId);
2062 + const userEmail = document.getElementById('user-email').value.trim();
2063 + const sessionId = getChatSession();
3448 2064
3449 - // Validate email
2065 + // Validate email before submission
3450 2066 if (!userEmail) {
3451 - showEmailError(botId, 'Please enter your email address.');
3452 - return false;
2067 + showEmailError('Please enter your email address.');
2068 + return;
3453 2069 }
3454 2070
3455 - if (!isValidEmailAddress(userEmail)) {
3456 - showEmailError(botId, 'Please enter a valid email address.');
3457 - return false;
2071 + if (!isValidEmail(userEmail)) {
2072 + showEmailError('Please enter a valid email address.');
2073 + return;
3458 2074 }
3459 2075
3460 - // Validate name if field exists and has content
3461 - if (nameInput && userName && !isValidNameInput(userName)) {
3462 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3463 - return false;
3464 - }
2076 + // Clear any existing errors
2077 + clearEmailError();
2078 + setSubmissionState(true);
3465 2079
3466 - clearEmailError(botId);
3467 - setEmailSubmissionState(botId, true);
2080 + // Add timeout for submission
2081 + const controller = new AbortController();
2082 + const timeoutId = setTimeout(() => {
2083 + controller.abort();
2084 + setSubmissionState(false);
2085 + showEmailError('Request timed out. Please try again.');
2086 + }, 15000); // 15 second timeout
3468 2087
3469 - // Prepare form data
3470 - const formData = new URLSearchParams({
3471 - action: 'mxchat_handle_save_email_and_response',
3472 - email: userEmail,
3473 - session_id: sessionId,
3474 - nonce: mxchatChat.nonce,
3475 - });
3476 -
3477 - if (userName) {
3478 - formData.append('name', userName);
3479 - }
3480 -
3481 2088 fetch(mxchatChat.ajax_url, {
3482 2089 method: 'POST',
3483 2090 headers: {
3484 2091 'Content-Type': 'application/x-www-form-urlencoded',
3485 2092 },
3486 - body: formData
2093 + body: new URLSearchParams({
2094 + action: 'mxchat_handle_save_email_and_response',
2095 + email: userEmail,
2096 + session_id: sessionId,
2097 + nonce: mxchatChat.nonce,
2098 + }),
2099 + signal: controller.signal
3487 2100 })
3488 2101 .then((response) => {
2102 + clearTimeout(timeoutId);
3489 2103 if (!response.ok) {
3490 2104 throw new Error(`HTTP error! status: ${response.status}`);
3491 2105 }
3492 2106 return response.json();
@@ -3491,166 +2105,185 @@
3491 2105 }
3492 2106 return response.json();
3493 2107 })
3494 2108 .then((data) => {
3495 - setEmailSubmissionState(botId, false);
3496 -
2109 + setSubmissionState(false);
2110 +
3497 2111 if (data.success) {
3498 - showChatContainerForBot(botId);
2112 + // Show chat immediately
2113 + showChatContainer();
3499 2114
3500 - // Replace {visitor_name} placeholder in intro message with actual name
3501 - if (userName) {
3502 - replaceVisitorNamePlaceholder(botId, userName);
3503 - } else {
3504 - // Remove placeholder if no name provided
3505 - replaceVisitorNamePlaceholder(botId, '');
3506 - }
3507 -
2115 + // Handle bot response if provided
3508 2116 if (data.message && typeof appendMessage === 'function') {
3509 2117 setTimeout(() => {
3510 - appendMessage('bot', data.message, '', [], false, botId);
2118 + appendMessage('bot', data.message);
3511 2119 if (typeof scrollToBottom === 'function') {
3512 - scrollToBottom(botId);
2120 + scrollToBottom();
3513 2121 }
3514 2122 }, 100);
3515 2123 }
3516 2124 } else {
3517 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2125 + showEmailError(data.message || 'Failed to save email. Please try again.');
3518 2126 }
3519 2127 })
3520 2128 .catch((error) => {
3521 - setEmailSubmissionState(botId, false);
3522 - showEmailError(botId, 'An error occurred. Please try again.');
2129 + clearTimeout(timeoutId);
2130 + setSubmissionState(false);
2131 +
2132 + if (error.name === 'AbortError') {
2133 + showEmailError('Request timed out. Please try again.');
2134 + } else {
2135 + console.error('Email submission error:', error);
2136 + showEmailError('An error occurred. Please try again.');
2137 + }
3523 2138 });
3524 -
3525 - return false;
3526 2139 });
3527 2140
3528 - // Real-time email validation using event delegation
3529 - $(document).on('input', '.mxchat-email-input', function() {
3530 - var botId = getBotIdFromElement(this);
3531 - var $input = $(this);
2141 + // Real-time email validation
2142 + const emailInput = document.getElementById('user-email');
2143 + if (emailInput) {
2144 + let validationTimeout;
2145 +
2146 + emailInput.addEventListener('input', function() {
2147 + // Clear previous validation timeout
2148 + if (validationTimeout) {
2149 + clearTimeout(validationTimeout);
2150 + }
2151 +
2152 + // Debounce validation
2153 + validationTimeout = setTimeout(() => {
2154 + const email = this.value.trim();
2155 + clearEmailError();
2156 +
2157 + if (email && !isValidEmail(email)) {
2158 + showEmailError('Please enter a valid email address.');
2159 + }
2160 + }, 500);
2161 + });
3532 2162
3533 - // Clear previous timeout
3534 - clearTimeout($input.data('validationTimeout'));
3535 -
3536 - // Debounce validation
3537 - var timeout = setTimeout(() => {
3538 - var email = this.value.trim();
3539 - clearEmailError(botId);
3540 -
3541 - if (email && !isValidEmailAddress(email)) {
3542 - showEmailError(botId, 'Please enter a valid email address.');
2163 + // Handle Enter key
2164 + emailInput.addEventListener('keypress', function(e) {
2165 + if (e.key === 'Enter' && !isSubmitting) {
2166 + emailForm.dispatchEvent(new Event('submit'));
3543 2167 }
3544 - }, 500);
2168 + });
2169 + }
3545 2170
3546 - $input.data('validationTimeout', timeout);
3547 - });
3548 -
3549 - // Handle Enter key in email input
3550 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3551 - if (e.key === 'Enter') {
3552 - e.preventDefault();
3553 - var botId = getBotIdFromElement(this);
3554 - if (!emailSubmittingState[botId]) {
3555 - $(this).closest('.email-collection-form').submit();
3556 - }
2171 + // Error display functions
2172 + function showEmailError(message) {
2173 + clearEmailError();
2174 +
2175 + const errorDiv = document.createElement('div');
2176 + errorDiv.className = 'email-error';
2177 + errorDiv.style.cssText = `
2178 + color: #e74c3c;
2179 + font-size: 12px;
2180 + margin-top: 8px;
2181 + padding: 4px 0;
2182 + animation: fadeInError 0.3s ease;
2183 + `;
2184 + errorDiv.textContent = message;
2185 +
2186 + // Add CSS animation if not already present
2187 + if (!document.getElementById('email-error-styles')) {
2188 + const style = document.createElement('style');
2189 + style.id = 'email-error-styles';
2190 + style.textContent = `
2191 + @keyframes fadeInError {
2192 + from { opacity: 0; transform: translateY(-5px); }
2193 + to { opacity: 1; transform: translateY(0); }
2194 + }
2195 + .email-input-shake {
2196 + animation: shake 0.5s ease-in-out;
2197 + }
2198 + @keyframes shake {
2199 + 0%, 100% { transform: translateX(0); }
2200 + 25% { transform: translateX(-5px); }
2201 + 75% { transform: translateX(5px); }
2202 + }
2203 + `;
2204 + document.head.appendChild(style);
3557 2205 }
3558 - });
3559 -
3560 - // Handle Enter key in name input
3561 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3562 - if (e.key === 'Enter') {
3563 - e.preventDefault();
3564 - var botId = getBotIdFromElement(this);
3565 - if (!emailSubmittingState[botId]) {
3566 - $(this).closest('.email-collection-form').submit();
3567 - }
2206 +
2207 + emailForm.appendChild(errorDiv);
2208 +
2209 + // Add shake animation to input
2210 + if (emailInput) {
2211 + emailInput.classList.add('email-input-shake');
2212 + setTimeout(() => {
2213 + emailInput.classList.remove('email-input-shake');
2214 + }, 500);
3568 2215 }
3569 - });
2216 + }
3570 2217
3571 - // Initialize email check for all bot instances
3572 - // For floating bots: defer until widget is opened (zero passive AJAX)
3573 - // For embedded bots: check immediately since the form is visible
3574 - $('.mxchat-chatbot-wrapper').each(function() {
3575 - var botId = $(this).data('bot-id') || 'default';
3576 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2218 + function clearEmailError() {
2219 + const existingErrors = emailForm.querySelectorAll('.email-error');
2220 + existingErrors.forEach(error => error.remove());
2221 + }
3577 2222
3578 - if (emailBlocker) {
3579 - if (isEmbeddedBot(botId)) {
3580 - // Embedded bots are always visible — check now
3581 - resolveEmailState(botId);
3582 - }
3583 - // Floating bots: handled in the widget open handler
3584 - } else if (isEmbeddedBot(botId)) {
3585 - // Embedded bot, no email collection — load history with loader
3586 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3587 - if (chatPersistenceEnabled) {
3588 - MxChatInstances.ensureSession(botId);
3589 - showChatContainerForBot(botId);
3590 - }
3591 - }
2223 + // Initialize email check with delay to prevent race conditions
2224 + setTimeout(checkSessionAndEmail, 100);
2225 +
2226 +} else if (mxchatChat.email_collection_enabled) {
2227 + console.error('Essential elements for email handling are missing:', {
2228 + emailForm: !!emailForm,
2229 + emailBlocker: !!emailBlocker,
2230 + chatbotWrapper: !!chatbotWrapper
3592 2231 });
3593 2232 }
3594 2233
3595 - // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3596 - $(document).on('click', '.pre-chat-message', function() {
3597 - var botId = getBotIdFromElement(this);
3598 - var $chatbot = getElement(botId, 'floating-chatbot');
3599 - if ($chatbot.hasClass('hidden')) {
3600 - $chatbot.removeClass('hidden').addClass('visible');
3601 - getElement(botId, 'floating-chatbot-button').addClass('hidden');
3602 - handlePreChatDismissal(botId);
2234 +
2235 + // Open chatbot when pre-chat message is clicked
2236 + $(document).on('click', '#pre-chat-message', function() {
2237 + var chatbot = $('#floating-chatbot');
2238 + if (chatbot.hasClass('hidden')) {
2239 + chatbot.removeClass('hidden').addClass('visible');
2240 + $('#floating-chatbot-button').addClass('hidden');
2241 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
3603 2242 disableScroll(); // Disable scroll when chatbot opens
3604 -
3605 - // Load chat history for returning visitors (persistence)
3606 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3607 - if (chatPersistenceEnabled) {
3608 - MxChatInstances.ensureSession(botId);
3609 - }
3610 -
3611 - // Deferred email check — only on first widget open
3612 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3613 - var instance = MxChatInstances.get(botId);
3614 - if (emailBlocker && !instance.emailCheckDone) {
3615 - instance.emailCheckDone = true;
3616 - resolveEmailState(botId);
3617 - } else if (!emailBlocker) {
3618 - showChatContainerForBot(botId);
3619 - }
3620 2243 }
3621 2244 });
3622 2245
3623 - // Legacy duplicate close handler removed — handled by single event delegation above
2246 + var closeButton = document.querySelector('.close-pre-chat-message');
2247 + if (closeButton) {
2248 + closeButton.addEventListener('click', function() {
2249 + $('#pre-chat-message').fadeOut(200); // Hide the message
3624 2250
2251 + // Send an AJAX request to set the transient flag for 24 hours
2252 + $.ajax({
2253 + url: mxchatChat.ajax_url,
2254 + type: 'POST',
2255 + data: {
2256 + action: 'mxchat_dismiss_pre_chat_message',
2257 + _ajax_nonce: mxchatChat.nonce
2258 + },
2259 + success: function() {
2260 + //console.log('Pre-chat message dismissed for 24 hours.');
3625 2261
3626 -function hasQuickQuestions(botId) {
3627 - botId = botId || 'default';
3628 - var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3629 - if (!questionsContainer) return false;
3630 - const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
2262 + // Ensure the message is hidden after dismissal
2263 + $('#pre-chat-message').hide();
2264 + },
2265 + error: function() {
2266 + ////console.error('Failed to dismiss pre-chat message.');
2267 + }
2268 + });
2269 + });
2270 + }
2271 +
2272 +
2273 +function hasQuickQuestions() {
2274 + const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
3631 2275 return questionButtons.length > 0;
3632 2276 }
3633 2277
3634 -/**
3635 - * Check if a bot is embedded (not floating)
3636 - * Embedded bots don't have a .floating-chatbot wrapper
3637 - */
3638 -function isEmbeddedBot(botId) {
3639 - botId = botId || 'default';
3640 - var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3641 - return !floatingWrapper;
3642 -}
3643 -
3644 -function collapseQuickQuestions(botId) {
3645 - botId = botId || 'default';
3646 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3647 - if (questionsContainer && hasQuickQuestions(botId)) {
2278 +function collapseQuickQuestions() {
2279 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2280 + if (questionsContainer && hasQuickQuestions()) {
3648 2281 questionsContainer.classList.add('collapsed');
3649 2282 questionsContainer.classList.add('has-been-collapsed');
3650 2283 try {
3651 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3652 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
2284 + sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2285 + sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
3653 2286 } catch (e) {
3654 2287 // Ignore if sessionStorage is not available
3655 2288 }
3656 2289 }
@@ -3655,15 +2288,14 @@
3655 2288 }
3656 2289 }
3657 2290 }
3658 2291
3659 -function expandQuickQuestions(botId) {
3660 - botId = botId || 'default';
3661 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3662 - if (questionsContainer && hasQuickQuestions(botId)) {
2292 +function expandQuickQuestions() {
2293 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2294 + if (questionsContainer && hasQuickQuestions()) {
3663 2295 questionsContainer.classList.remove('collapsed');
3664 2296 try {
3665 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
2297 + sessionStorage.setItem('mxchat_questions_collapsed', 'false');
3666 2298 } catch (e) {
3667 2299 // Ignore if sessionStorage is not available
3668 2300 }
3669 2301 }
@@ -3668,24 +2300,18 @@
3668 2300 }
3669 2301 }
3670 2302 }
3671 2303
3672 -function checkQuickQuestionsState(botId) {
3673 - botId = botId || 'default';
3674 - if (!hasQuickQuestions(botId)) {
2304 +function checkQuickQuestionsState() {
2305 + if (!hasQuickQuestions()) {
3675 2306 return; // Don't do anything if no questions exist
3676 2307 }
3677 -
3678 - // Skip restoring collapsed state for embedded bots - they should always start expanded
3679 - if (isEmbeddedBot(botId)) {
3680 - return;
3681 - }
3682 -
2308 +
3683 2309 try {
3684 - const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3685 - const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3686 -
3687 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
2310 + const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2311 + const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2312 +
2313 + const questionsContainer = document.getElementById('mxchat-popular-questions');
3688 2314 if (questionsContainer) {
3689 2315 if (hasBeenCollapsed === 'true') {
3690 2316 questionsContainer.classList.add('has-been-collapsed');
3691 2317 }
@@ -3696,100 +2322,39 @@
3696 2322 } catch (e) {
3697 2323 // Ignore if sessionStorage is not available
3698 2324 }
3699 2325 }
2326 +
2327 +
2328 +
2329 +
2330 +// ====================================
2331 +// MAIN INITIALIZATION
2332 +// ====================================
3700 2333
3701 -// Global delegation for dynamically added links as fallback
3702 -// Use class selector for multi-instance support
3703 -$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
3704 - const $link = $(this);
3705 - const messageDiv = $link.closest('.bot-message, .agent-message');
2334 +if ($('#floating-chatbot').hasClass('hidden')) {
2335 + $('#floating-chatbot-button').removeClass('hidden');
2336 +}
2337 +// Initialize when document is ready
2338 +setFullHeight();
2339 +initializeChatVisibility();
2340 +loadChatHistory();
3706 2341
3707 - // Only process bot/agent message links
3708 - if (messageDiv.length > 0) {
3709 - const originalHref = $link.attr('href');
2342 +// Make functions globally available for add-ons
2343 +window.hasQuickQuestions = hasQuickQuestions;
2344 +window.collapseQuickQuestions = collapseQuickQuestions;
2345 +window.appendMessage = appendMessage;
2346 +window.appendThinkingMessage = appendThinkingMessage;
2347 +window.scrollToBottom = scrollToBottom;
2348 +window.scrollElementToTop = scrollElementToTop;
2349 +window.replaceLastMessage = replaceLastMessage;
2350 +window.callMxChat = callMxChat;
2351 +window.callMxChatStream = callMxChatStream;
2352 +window.shouldUseStreaming = shouldUseStreaming;
2353 +window.getChatSession = getChatSession;
2354 +window.getPageContext = getPageContext;
2355 +window.updateStreamingMessage = updateStreamingMessage;
3710 2356
3711 - if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3712 - e.preventDefault();
3713 - e.stopPropagation();
3714 -
3715 - // Mark as tracked
3716 - $link.attr('data-tracked', 'true');
3717 -
3718 - // Get bot ID from the chat box context
3719 - var botId = getBotIdFromElement(this);
3720 -
3721 - // Get message context from the message div
3722 - const messageText = messageDiv.text().substring(0, 200);
3723 -
3724 - $.ajax({
3725 - url: mxchatChat.ajax_url,
3726 - type: 'POST',
3727 - data: {
3728 - action: 'mxchat_track_url_click',
3729 - session_id: getChatSession(botId),
3730 - url: originalHref,
3731 - message_context: messageText,
3732 - nonce: mxchatChat.nonce
3733 - },
3734 - complete: function() {
3735 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3736 - window.open(originalHref, '_blank');
3737 - } else {
3738 - window.location.href = originalHref;
3739 - }
3740 - }
3741 - });
3742 -
3743 - return false;
3744 - }
3745 - }
3746 -});
3747 -
3748 - // ====================================
3749 - // MAIN INITIALIZATION
3750 - // ====================================
3751 -
3752 - // Initialize all chatbot instances on the page
3753 - initializeAllInstances();
3754 -
3755 - // Legacy initialization for single bot compatibility
3756 - $('.floating-chatbot.hidden').each(function() {
3757 - var botId = getBotIdFromElement(this);
3758 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3759 - });
3760 -
3761 - // Initialize when document is ready
3762 - setFullHeight();
3763 -
3764 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3765 - // until the user's first interaction via MxChatInstances.ensureSession()
3766 -
3767 - // Initialize chat visibility for all instances
3768 - $('.mxchat-chatbot-wrapper').each(function() {
3769 - var botId = $(this).data('bot-id') || 'default';
3770 - initializeChatVisibility(botId);
3771 - });
3772 -
3773 - // Make functions globally available for add-ons
3774 - window.hasQuickQuestions = hasQuickQuestions;
3775 - window.collapseQuickQuestions = collapseQuickQuestions;
3776 - window.appendMessage = appendMessage;
3777 - window.appendThinkingMessage = appendThinkingMessage;
3778 - window.scrollToBottom = scrollToBottom;
3779 - window.scrollElementToTop = scrollElementToTop;
3780 - window.replaceLastMessage = replaceLastMessage;
3781 - window.callMxChat = callMxChat;
3782 - window.callMxChatStream = callMxChatStream;
3783 - window.shouldUseStreaming = shouldUseStreaming;
3784 - window.getChatSession = getChatSession;
3785 - window.getPageContext = getPageContext;
3786 - window.updateStreamingMessage = updateStreamingMessage;
3787 - window.MxChatInstances = MxChatInstances;
3788 - window.getElement = getElement;
3789 - window.getElementDOM = getElementDOM;
3790 - window.getBotIdFromElement = getBotIdFromElement;
3791 -
3792 2357 }); // End of jQuery ready
3793 2358
3794 2359
3795 2360 // ====================================
@@ -3817,5 +2382,10 @@
3817 2382 });
3818 2383 }
3819 2384 }
3820 2385 });
2386 +
2387 +
2388 +
2389 +
2390 +
3821 2391