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