PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.4
MxChat – AI Chatbot & Content Generation for WordPress v3.2.4
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / chat-script.js

chat-script.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.4, at js/chat-script.js

3,575 lines 140.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
20 // ====================================
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 // GLOBAL VARIABLES & CONFIGURATION
221 // ====================================
222 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
223
224 // Initialize color settings (these are global as they come from PHP)
225 var userMessageBgColor = mxchatChat.user_message_bg_color;
226 var userMessageFontColor = mxchatChat.user_message_font_color;
227 var botMessageBgColor = mxchatChat.bot_message_bg_color;
228 var botMessageFontColor = mxchatChat.bot_message_font_color;
229 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
230 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
231
232 var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
233
234 // ====================================
235 // SESSION MANAGEMENT (Legacy compatibility)
236 // ====================================
237
238 function getCookie(name) {
239 let value = "; " + document.cookie;
240 let parts = value.split("; " + name + "=");
241 if (parts.length == 2) return parts.pop().split(";").shift();
242 }
243
244 function generateSessionId() {
245 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
246 }
247
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 // ====================================
288 // CONTEXTUAL AWARENESS FUNCTIONALITY
289 // ====================================
290
291 function getPageContext() {
292 // Check if contextual awareness is enabled
293 if (mxchatChat.contextual_awareness_toggle !== 'on') {
294 return null;
295 }
296
297 // Get page URL
298 const pageUrl = window.location.href;
299
300 // Get page title
301 const pageTitle = document.title || '';
302
303 // Get main content from the page
304 let pageContent = '';
305
306 // Try to get content from common content areas
307 const contentSelectors = [
308 'main',
309 '[role="main"]',
310 '.content',
311 '.main-content',
312 '.post-content',
313 '.entry-content',
314 '.page-content',
315 'article',
316 '#content',
317 '#main'
318 ];
319
320 let contentElement = null;
321 for (const selector of contentSelectors) {
322 contentElement = document.querySelector(selector);
323 if (contentElement) {
324 break;
325 }
326 }
327
328 // If no specific content area found, use body but exclude header, footer, nav, sidebar
329 if (!contentElement) {
330 contentElement = document.body;
331 }
332
333 if (contentElement) {
334 // Clone the element to avoid modifying the original
335 const clone = contentElement.cloneNode(true);
336
337 // Remove unwanted elements
338 const unwantedSelectors = [
339 'header',
340 'footer',
341 'nav',
342 '.navigation',
343 '.sidebar',
344 '.widget',
345 '.menu',
346 'script',
347 'style',
348 '.comments',
349 '#comments',
350 '.breadcrumb',
351 '.breadcrumbs',
352 '#floating-chatbot',
353 '#floating-chatbot-button',
354 '.mxchat',
355 '[class*="chat"]',
356 '[id*="chat"]'
357 ];
358
359 unwantedSelectors.forEach(selector => {
360 const elements = clone.querySelectorAll(selector);
361 elements.forEach(el => el.remove());
362 });
363
364 // Extract MxChat context data attributes before getting text content
365 const contextData = [];
366 clone.querySelectorAll('[data-mxchat-context]').forEach(el => {
367 const contextValue = el.dataset.mxchatContext;
368 if (contextValue && contextValue.trim()) {
369 contextData.push(contextValue);
370 }
371 });
372
373 // Get text content and clean it up
374 pageContent = clone.textContent || clone.innerText || '';
375
376 // Add context data to page content if any were found
377 if (contextData.length > 0) {
378 pageContent += '\n\nAdditional Context:\n' + contextData.join('\n');
379 }
380
381 // Clean up whitespace and limit length
382 pageContent = pageContent
383 .replace(/\s+/g, ' ')
384 .trim()
385 .substring(0, 3000); // Limit to 3000 characters to avoid token limits
386 }
387
388 // Only return context if we have meaningful content
389 if (!pageContent || pageContent.length < 50) {
390 return null;
391 }
392
393 return {
394 url: pageUrl,
395 title: pageTitle,
396 content: pageContent
397 };
398 }
399
400 // Track originating page when chat starts
401 function trackOriginatingPage() {
402 const sessionId = getChatSession();
403 const pageUrl = window.location.href;
404 const pageTitle = document.title || 'Untitled Page';
405
406 // Only track once per session
407 const trackingKey = 'mxchat_originating_tracked_' + sessionId;
408 if (sessionStorage.getItem(trackingKey)) {
409 return;
410 }
411
412 $.ajax({
413 url: mxchatChat.ajax_url,
414 type: 'POST',
415 data: {
416 action: 'mxchat_track_originating_page',
417 session_id: sessionId,
418 page_url: pageUrl,
419 page_title: pageTitle,
420 nonce: mxchatChat.nonce
421 },
422 success: function(response) {
423 if (response.success) {
424 sessionStorage.setItem(trackingKey, 'true');
425 }
426 }
427 });
428 }
429
430 // ====================================
431 // CORE CHAT FUNCTIONALITY
432 // ====================================
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 // 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
473 // ADD PROMPT HOOK HERE
474 if (typeof customMxChatFilter === 'function') {
475 message = customMxChatFilter(message, "prompt");
476 }
477
478 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 }
485
486 appendMessage("user", message, '', [], false, botId);
487 $chatInput.val('');
488 $chatInput.css('height', 'auto');
489
490 if (hasQuickQuestions(botId)) {
491 collapseQuickQuestions(botId);
492 }
493 appendThinkingMessage(botId);
494 scrollToBottom(botId);
495
496 const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
497
498 // Check if streaming is enabled AND supported for this model
499 if (shouldUseStreaming(currentModel)) {
500 callMxChatStream(message, function(response) {
501 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
502 }, botId);
503 } else {
504 callMxChat(message, function(response) {
505 replaceLastMessage("bot", response, '', [], botId);
506 }, botId);
507 }
508 }
509 }
510
511 // Update your existing sendMessageToChatbot function
512 function sendMessageToChatbot(message, botId) {
513 botId = botId || 'default';
514 MxChatInstances.ensureSession(botId);
515
516 // ADD PROMPT HOOK HERE
517 if (typeof customMxChatFilter === 'function') {
518 message = customMxChatFilter(message, "prompt");
519 }
520
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);
526 }
527
528 var sessionId = getChatSession(botId);
529
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 // Check if streaming is enabled AND supported for this model
539 if (shouldUseStreaming(currentModel)) {
540 callMxChatStream(message, function(response) {
541 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
542 }, botId);
543 } else {
544 callMxChat(message, function(response) {
545 getElement(botId, 'chat-box').find('.temporary-message').remove();
546 replaceLastMessage("bot", response, '', [], botId);
547 }, botId);
548 }
549 }
550
551 // Updated shouldUseStreaming function with debugging
552 function shouldUseStreaming(model) {
553 // Check if streaming is enabled in settings (using your toggle naming pattern)
554 const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
555
556 // Check if model supports streaming
557 const streamingSupported = isStreamingSupported(model);
558
559
560 // Only use streaming if both enabled and supported
561 return streamingEnabled && streamingSupported;
562 }
563
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
610 // Get page context if contextual awareness is enabled
611 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
626 // Prepare AJAX data
627 const ajaxData = {
628 action: 'mxchat_handle_chat_request',
629 message: message,
630 session_id: sessionId,
631 nonce: mxchatChat.nonce,
632 current_page_url: window.location.href,
633 current_page_title: document.title,
634 bot_id: botId,
635 // Pass session start timestamp so AI context matches what user sees
636 session_start_timestamp: instance.sessionStartTimestamp || 0
637 };
638
639 // Add page context if available
640 if (pageContext) {
641 ajaxData.page_context = JSON.stringify(pageContext);
642 }
643
644 // CHECK FOR VISION FLAGS AND ADD THEM
645 if (window.mxchatVisionProcessed) {
646 ajaxData.vision_processed = true;
647 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
648 ajaxData.vision_images_count = window.mxchatVisionImagesCount || 0;
649 // Clear the flags after use
650 window.mxchatVisionProcessed = false;
651 window.mxchatOriginalMessage = null;
652 window.mxchatVisionImagesCount = 0;
653 }
654
655 $.ajax({
656 url: mxchatChat.ajax_url,
657 type: 'POST',
658 dataType: 'json',
659 data: ajaxData,
660 success: function(response) {
661 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
662 if (response.chat_mode) {
663 updateChatModeIndicator(response.chat_mode, botId);
664 }
665
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
729 // This preserves compatibility with your server response format
730 if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
731 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
732
733 // Handle successful response - this is your original success handling code
734
735 // Handle other responses
736 let responseText = response.text || '';
737 let responseHtml = response.html || '';
738 let responseMessage = response.message || '';
739
740 // Add PDF filename handling
741 if (response.data && response.data.filename) {
742 showActivePdf(response.data.filename, botId);
743 var instance = MxChatInstances.get(botId);
744 instance.activePdfFile = response.data.filename;
745 }
746
747 // Add redirect check here
748 if (response.redirect_url) {
749 if (responseText) {
750 replaceLastMessage("bot", responseText, '', [], botId);
751 }
752 setTimeout(() => {
753 window.location.href = response.redirect_url;
754 }, 1500);
755 return;
756 }
757
758 // Check for live agent response
759 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 removeThinkingDots(botId);
761 updateChatModeIndicator('agent', botId);
762 enableChatInput(botId);
763 return;
764 }
765
766 // Handle the message and show notification if chat is hidden
767 if (responseText || responseHtml || responseMessage) {
768
769 // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
770 if (responseText && typeof customMxChatFilter === 'function') {
771 responseText = customMxChatFilter(responseText, "response");
772 }
773 if (responseMessage && typeof customMxChatFilter === 'function') {
774 responseMessage = customMxChatFilter(responseMessage, "response");
775 }
776
777 // Update the messages as before
778 if (responseText && responseHtml) {
779 replaceLastMessage("bot", responseText, responseHtml, [], botId);
780 } else if (responseText) {
781 replaceLastMessage("bot", responseText, '', [], botId);
782 } else if (responseHtml) {
783 replaceLastMessage("bot", "", responseHtml, [], botId);
784 } else if (responseMessage) {
785 replaceLastMessage("bot", responseMessage, '', [], botId);
786 }
787
788 // 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();
794 }
795 }
796 } 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);
802 }
803
804 if (response.message_id) {
805 var instance = MxChatInstances.get(botId);
806 instance.lastSeenMessageId = response.message_id;
807 }
808
809 return;
810 }
811
812 // Fallback for truly unexpected response formats
813 replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
814 },
815 error: function(xhr, status, error) {
816 let errorMessage = "An unexpected error occurred.";
817
818 // Try to parse the response if it's JSON
819 try {
820 const responseJson = JSON.parse(xhr.responseText);
821
822 if (responseJson.data && responseJson.data.error_message) {
823 errorMessage = responseJson.data.error_message;
824 } else if (responseJson.message) {
825 errorMessage = responseJson.message;
826 }
827 } catch (e) {
828 // Not JSON or parsing failed, use HTTP status based messages
829 if (xhr.status === 0) {
830 errorMessage = "Network error: Please check your internet connection.";
831 } else if (xhr.status === 403) {
832 errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
833 } else if (xhr.status === 404) {
834 errorMessage = "API endpoint not found. Please contact support.";
835 } else if (xhr.status === 429) {
836 errorMessage = "Too many requests. Please try again in a moment.";
837 } else if (xhr.status >= 500) {
838 errorMessage = "Server error: The server encountered an issue. Please try again later.";
839 }
840 }
841
842 replaceLastMessage("bot", errorMessage, '', [], botId);
843 }
844 });
845 }
846
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';
854 if (!isStreamingSupported(currentModel)) {
855 callMxChat(message, callback, botId);
856 return;
857 }
858
859 // Get page context if contextual awareness is enabled
860 const pageContext = getPageContext();
861
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 const formData = new FormData();
876 formData.append('action', 'mxchat_stream_chat');
877 formData.append('message', message);
878 formData.append('session_id', streamSessionId);
879 formData.append('nonce', mxchatChat.nonce);
880 formData.append('current_page_url', window.location.href);
881 formData.append('current_page_title', document.title);
882 formData.append('bot_id', botId);
883 // Pass session start timestamp so AI context matches what user sees
884 formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
885
886 // Add page context if available
887 if (pageContext) {
888 formData.append('page_context', JSON.stringify(pageContext));
889 }
890
891 // CHECK FOR VISION FLAGS AND ADD THEM
892 if (window.mxchatVisionProcessed) {
893 formData.append('vision_processed', 'true');
894 formData.append('original_user_message', window.mxchatOriginalMessage || message);
895 formData.append('vision_images_count', window.mxchatVisionImagesCount || '0');
896 // Clear the flags after use
897 window.mxchatVisionProcessed = false;
898 window.mxchatOriginalMessage = null;
899 window.mxchatVisionImagesCount = 0;
900 }
901
902 let accumulatedContent = '';
903 let testingDataReceived = false;
904 let streamingStarted = false;
905
906 fetch(mxchatChat.ajax_url, {
907 method: 'POST',
908 body: formData,
909 credentials: 'same-origin'
910 })
911 .then(response => {
912 // Store the response for potential fallback handling
913 const responseClone = response.clone();
914
915 if (!response.ok) {
916 // Try to get error details from response
917 return responseClone.json().then(errorData => {
918 throw { isServerError: true, data: errorData };
919 }).catch(() => {
920 throw new Error('Network response was not ok');
921 });
922 }
923
924 // Check if response is JSON instead of streaming
925 const contentType = response.headers.get('content-type');
926 if (contentType && contentType.includes('application/json')) {
927 return responseClone.json().then(data => {
928 // IMMEDIATE CHAT MODE UPDATE for JSON response
929 if (data.chat_mode) {
930 updateChatModeIndicator(data.chat_mode, botId);
931 }
932
933 // Check for testing panel
934 if (window.mxchatTestPanelInstance && data.testing_data) {
935 window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
936 }
937
938 // Handle the JSON response directly
939 handleNonStreamResponse(data, callback, botId);
940 return Promise.resolve(); // Prevent further processing
941 });
942 }
943
944 // Continue with streaming processing
945 const reader = response.body.getReader();
946 const decoder = new TextDecoder();
947 let buffer = '';
948
949 function processStream() {
950 reader.read().then(({ done, value }) => {
951 if (done) {
952 // If streaming completed but no content was received, try to get response as fallback
953 if (!streamingStarted || !accumulatedContent) {
954 // Try to read the response as JSON
955 responseClone.text().then(text => {
956 try {
957 const data = JSON.parse(text);
958 if (data.text || data.message || data.html) {
959 handleNonStreamResponse(data, callback, botId);
960 } else {
961 // No valid data, fall back to regular call
962 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
963 callMxChat(message, callback, botId);
964 }
965 } catch (e) {
966 // Could not parse, fall back to regular call
967 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
968 callMxChat(message, callback, botId);
969 }
970 }).catch(() => {
971 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
972 callMxChat(message, callback, botId);
973 });
974 return;
975 }
976
977 // Re-enable chat input when stream ends with content
978 enableChatInput(botId);
979
980 // Scroll the user's last message to the top now that the
981 // bot's full reply has rendered (gives max reading room).
982 var $chatBoxDone = getElement(botId, 'chat-box');
983 var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
984 if ($lastUserMsgDone.length) {
985 scrollElementToTop($lastUserMsgDone, botId);
986 }
987
988 if (callback) {
989 callback(accumulatedContent);
990 }
991 return;
992 }
993
994 buffer += decoder.decode(value, { stream: true });
995 const lines = buffer.split('\n');
996 buffer = lines.pop() || '';
997
998 for (const line of lines) {
999 if (line.startsWith('data: ')) {
1000 const data = line.substring(6);
1001
1002 if (data === '[DONE]') {
1003 if (!accumulatedContent) {
1004 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1005 callMxChat(message, callback, botId);
1006 return;
1007 }
1008
1009 // Re-enable chat input after streaming completes
1010 enableChatInput(botId);
1011
1012 // Scroll the user's last message to the top now
1013 // that the bot's full reply has rendered.
1014 var $chatBoxStreamDone = getElement(botId, 'chat-box');
1015 var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1016 if ($lastUserMsgStreamDone.length) {
1017 scrollElementToTop($lastUserMsgStreamDone, botId);
1018 }
1019
1020 if (callback) {
1021 callback(accumulatedContent);
1022 }
1023 return;
1024 }
1025
1026 try {
1027 const json = JSON.parse(data);
1028
1029 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
1030 if (json.chat_mode) {
1031 updateChatModeIndicator(json.chat_mode, botId);
1032 }
1033
1034 // Handle testing data
1035 if (json.testing_data && !testingDataReceived) {
1036 if (window.mxchatTestPanelInstance) {
1037 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
1038 testingDataReceived = true;
1039 }
1040 }
1041 // Handle content streaming
1042 else if (json.content) {
1043 streamingStarted = true;
1044 accumulatedContent += json.content;
1045 updateStreamingMessage(accumulatedContent, botId);
1046 }
1047 // Handle complete response in stream (fallback response)
1048 else if (json.text || json.message || json.html) {
1049 handleNonStreamResponse(json, callback, botId);
1050 return;
1051 }
1052 // Handle errors
1053 else if (json.error) {
1054
1055 // Get error message from various possible fields
1056 let errorMessage = json.error_message || json.message || json.text ||
1057 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1058
1059 // Re-enable chat input on error
1060 enableChatInput(botId);
1061
1062 // Display the error directly in the chat
1063 replaceLastMessage("bot", errorMessage, '', [], botId);
1064
1065 if (callback) {
1066 callback(errorMessage);
1067 }
1068 return;
1069 }
1070 } catch (e) {
1071 // SSE data parsing error - silently continue
1072 }
1073 }
1074 }
1075
1076 processStream();
1077 }).catch(streamError => {
1078 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1079 callMxChat(message, callback, botId);
1080 });
1081 }
1082
1083 processStream();
1084 })
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 });
1100 }
1101
1102 // Helper function to handle non-streaming responses
1103 function handleNonStreamResponse(data, callback, botId) {
1104 botId = botId || 'default';
1105
1106 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1107 if (data.chat_mode) {
1108 updateChatModeIndicator(data.chat_mode, botId);
1109 }
1110
1111 // Also check in data property if response is wrapped
1112 if (data.data && data.data.chat_mode) {
1113 updateChatModeIndicator(data.data.chat_mode, botId);
1114 }
1115
1116 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1117 // This prevents a visual gap between thinking dots disappearing and content appearing
1118
1119 // SECURITY FIX: Check for errors FIRST
1120 if (data.success === false || (data.data && data.data.error_message)) {
1121 let errorMessage = "";
1122 let errorCode = "";
1123
1124 // Check various possible error locations
1125 if (data.data && data.data.error_message) {
1126 errorMessage = data.data.error_message;
1127 errorCode = data.data.error_code || "";
1128 } else if (data.error_message) {
1129 errorMessage = data.error_message;
1130 errorCode = data.error_code || "";
1131 } else if (data.message) {
1132 errorMessage = data.message;
1133 } else if (typeof data.data === 'string') {
1134 errorMessage = data.data;
1135 } else {
1136 errorMessage = "An error occurred. Please try again or contact support.";
1137 }
1138
1139 // Handle session reset action (IP changed, session expired, etc.)
1140 // Silent reset — keep chat UI intact, just get a new session and retry
1141 if (data.data && data.data.action === 'reset_session') {
1142 MxChatInstances.silentResetSession(botId);
1143 // Re-send the original message with the new session (user message is already displayed)
1144 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 if (originalMessage) {
1146 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1148 if (shouldUseStreaming(currentModel)) {
1149 callMxChatStream(originalMessage, callback, botId);
1150 } else {
1151 callMxChat(originalMessage, callback, botId);
1152 }
1153 }
1154 return;
1155 }
1156
1157 // Format user-friendly error message
1158 let displayMessage = errorMessage;
1159 if (mxchatChat.is_admin) {
1160 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1161 }
1162
1163 replaceLastMessage("bot", displayMessage, '', [], botId);
1164
1165 if (callback) {
1166 callback('');
1167 }
1168 return; // Exit early for errors
1169 }
1170
1171 // Check for live agent response
1172 if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1173 removeThinkingDots(botId);
1174 // Also remove any leftover bot-message that lost its temporary-message class
1175 var $chatBox = getElement(botId, 'chat-box');
1176 $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1177 updateChatModeIndicator('agent', botId);
1178 enableChatInput(botId);
1179 if (callback) {
1180 callback('');
1181 }
1182 return;
1183 }
1184
1185 // Handle different response formats
1186 if (data.text || data.html || data.message) {
1187
1188 // Apply response hooks
1189 if (data.text && typeof customMxChatFilter === 'function') {
1190 data.text = customMxChatFilter(data.text, "response");
1191 }
1192 if (data.message && typeof customMxChatFilter === 'function') {
1193 data.message = customMxChatFilter(data.message, "response");
1194 }
1195
1196 // Display the response
1197 if (data.text && data.html) {
1198 replaceLastMessage("bot", data.text, data.html, [], botId);
1199 } else if (data.text) {
1200 replaceLastMessage("bot", data.text, '', [], botId);
1201 } else if (data.html) {
1202 replaceLastMessage("bot", "", data.html, [], botId);
1203 } else if (data.message) {
1204 replaceLastMessage("bot", data.message, '', [], botId);
1205 }
1206 }
1207
1208 // Handle other response properties
1209 if (data.data && data.data.filename) {
1210 showActivePdf(data.data.filename, botId);
1211 var instance = MxChatInstances.get(botId);
1212 instance.activePdfFile = data.data.filename;
1213 }
1214
1215 if (data.redirect_url) {
1216 setTimeout(() => {
1217 window.location.href = data.redirect_url;
1218 }, 1500);
1219 }
1220
1221 // Ensure chat input is re-enabled (safety net for edge cases)
1222 enableChatInput(botId);
1223
1224 if (callback) {
1225 callback(data.text || data.message || '');
1226 }
1227 }
1228
1229 // Enhanced updateChatModeIndicator function for immediate DOM updates
1230 function updateChatModeIndicator(mode, botId) {
1231 botId = botId || 'default';
1232 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1233 if (indicator) {
1234 const oldText = indicator.textContent;
1235
1236 if (mode === 'agent') {
1237 indicator.textContent = 'Live Agent';
1238 startPolling(botId);
1239 } else {
1240 // Everything else is AI mode
1241 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1242 indicator.textContent = customAiText;
1243 stopPolling(botId);
1244 }
1245
1246 // Force immediate DOM update and reflow
1247 if (oldText !== indicator.textContent) {
1248 // Force a reflow to ensure the change is visible immediately
1249 indicator.style.display = 'none';
1250 indicator.offsetHeight; // Trigger reflow
1251 indicator.style.display = '';
1252
1253 // Double-check after a brief moment to ensure the change stuck
1254 setTimeout(() => {
1255 if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
1256 indicator.textContent = 'Live Agent';
1257 } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
1258 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1259 indicator.textContent = customAiText;
1260 }
1261 }, 50);
1262 }
1263 }
1264 }
1265
1266 // Function to update message during streaming
1267 function updateStreamingMessage(content, botId) {
1268 botId = botId || 'default';
1269
1270 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1271 if (typeof customMxChatFilter === 'function') {
1272 content = customMxChatFilter(content, "response");
1273 }
1274
1275 const formattedContent = linkify(content);
1276
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 if (tempMessage.length) {
1282 // Update existing message
1283 tempMessage.html(formattedContent);
1284 } else {
1285 // Create new temporary message if it doesn't exist
1286 appendMessage("bot", content, '', [], true, botId);
1287 }
1288 }
1289
1290 function isStreamingSupported(model) {
1291 if (!model) return false;
1292
1293 const modelPrefix = model.split('-')[0].toLowerCase();
1294
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
1302
1303 return isSupported;
1304 }
1305
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);
1315 });
1316
1317 // Override enter key handler (using event delegation)
1318 $(document).on('keypress', '.chat-input', function(e) {
1319 if (e.which == 13 && !e.shiftKey) {
1320 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);
1327 }
1328 });
1329
1330
1331 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1332 try {
1333 // Determine styles based on sender type
1334 let messageClass, bgColor, fontColor;
1335
1336 if (sender === "user") {
1337 messageClass = "user-message";
1338 bgColor = userMessageBgColor;
1339 fontColor = userMessageFontColor;
1340 // Only sanitize user input
1341 messageText = sanitizeUserInput(messageText);
1342 } else if (sender === "agent") {
1343 messageClass = "agent-message";
1344 bgColor = liveAgentMessageBgColor;
1345 fontColor = liveAgentMessageFontColor;
1346 } else {
1347 messageClass = "bot-message";
1348 bgColor = botMessageBgColor;
1349 fontColor = botMessageFontColor;
1350 }
1351
1352 const messageDiv = $('<div>')
1353 .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({
1364 'background': bgColor,
1365 'color': fontColor,
1366 'margin-bottom': '1em'
1367 });
1368 }
1369
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 // Add images if provided
1376 if (images && images.length > 0) {
1377 fullMessage += '<div class="image-gallery" dir="auto">';
1378 images.forEach(img => {
1379 const safeTitle = sanitizeUserInput(img.title);
1380 const safeUrl = encodeURI(img.image_url);
1381 const safeThumbnail = encodeURI(img.thumbnail_url);
1382
1383 fullMessage += `
1384 <div style="margin-bottom: 10px;">
1385 <strong>${safeTitle}</strong><br>
1386 <a href="${safeUrl}" target="_blank">
1387 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
1388 </a>
1389 </div>`;
1390 });
1391 fullMessage += '</div>';
1392 }
1393
1394 // Append HTML content if provided
1395 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 }
1402 }
1403
1404 messageDiv.html(fullMessage);
1405
1406 if (isTemporary) {
1407 messageDiv.addClass('temporary-message');
1408 }
1409
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() {
1413 // FIXED: Use event delegation for link tracking
1414 if (sender === "bot" || sender === "agent") {
1415 attachLinkTracking(messageDiv, messageText, botId);
1416 }
1417
1418 if (sender === "bot") {
1419 const lastUserMessage = $chatBox.find('.user-message').last();
1420 if (lastUserMessage.length) {
1421 scrollElementToTop(lastUserMessage, botId);
1422 }
1423 }
1424 });
1425
1426 if (messageText.id) {
1427 var instance = MxChatInstances.get(botId);
1428 instance.lastSeenMessageId = messageText.id;
1429 hideNotification(botId);
1430 }
1431 } catch (error) {
1432 // Error rendering message - silently continue
1433 }
1434 }
1435
1436 // Helper function to attach link tracking with proper event handling
1437 function attachLinkTracking(messageDiv, messageText, botId) {
1438 botId = botId || 'default';
1439 // Use a slight delay to ensure DOM is ready
1440 setTimeout(function() {
1441 const links = messageDiv.find('a[href]').not('[data-tracked]');
1442
1443 links.each(function() {
1444 const $link = $(this);
1445 const originalHref = $link.attr('href');
1446
1447 // Mark as tracked to avoid duplicate handlers
1448 $link.attr('data-tracked', 'true');
1449
1450 // Only track external URLs
1451 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1452 // Remove any existing click handlers first
1453 $link.off('click.tracking');
1454
1455 // Add new click handler with namespace
1456 $link.on('click.tracking', function(e) {
1457 e.preventDefault();
1458 e.stopPropagation();
1459
1460 const messageContext = typeof messageText === 'string'
1461 ? messageText.substring(0, 200)
1462 : '';
1463
1464 // Track the click
1465 $.ajax({
1466 url: mxchatChat.ajax_url,
1467 type: 'POST',
1468 data: {
1469 action: 'mxchat_track_url_click',
1470 session_id: getChatSession(botId),
1471 url: originalHref,
1472 message_context: messageContext,
1473 nonce: mxchatChat.nonce
1474 },
1475 complete: function() {
1476 // Always redirect, even if tracking fails
1477 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1478 window.open(originalHref, '_blank');
1479 } else {
1480 window.location.href = originalHref;
1481 }
1482 }
1483 });
1484
1485 return false; // Extra insurance to prevent default
1486 });
1487 }
1488 });
1489 }, 100); // Small delay to ensure DOM is ready
1490 }
1491
1492 function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1493 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();
1496
1497 // Determine styles
1498 let bgColor, fontColor;
1499 if (sender === "user") {
1500 bgColor = userMessageBgColor;
1501 fontColor = userMessageFontColor;
1502 } else if (sender === "agent") {
1503 bgColor = liveAgentMessageBgColor;
1504 fontColor = liveAgentMessageFontColor;
1505 } else {
1506 bgColor = botMessageBgColor;
1507 fontColor = botMessageFontColor;
1508 }
1509
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
1515 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 }
1522 }
1523
1524 if (images.length > 0) {
1525 fullMessage += '<div class="image-gallery" dir="auto">';
1526 images.forEach(img => {
1527 fullMessage += `
1528 <div style="margin-bottom: 10px;">
1529 <strong>${img.title}</strong><br>
1530 <a href="${img.image_url}" target="_blank">
1531 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1532 </a>
1533 </div>`;
1534 });
1535 fullMessage += '</div>';
1536 }
1537
1538 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);
1572 } else {
1573 appendMessage(sender, responseText, responseHtml, images, false, botId);
1574 // Re-enable chat input after response is displayed
1575 enableChatInput(botId);
1576 }
1577 }
1578
1579
1580 function appendThinkingMessage(botId) {
1581 botId = botId || 'default';
1582
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 // Retrieve the bot message font color and background color
1598 var botMessageFontColor = mxchatChat.bot_message_font_color;
1599 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1600
1601 // Build thinking dots HTML - skip inline colors if AI theme is active
1602 var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1603 var thinkingHtml = '<div class="thinking-dots-container">' +
1604 '<div class="thinking-dots">' +
1605 '<span class="dot"' + dotStyle + '></span>' +
1606 '<span class="dot"' + dotStyle + '></span>' +
1607 '<span class="dot"' + dotStyle + '></span>' +
1608 '</div>' +
1609 '</div>';
1610
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);
1615 }
1616
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
1625 // ====================================
1626 // TEXT FORMATTING & PROCESSING
1627 // ====================================
1628
1629 function linkify(inputText) {
1630 if (!inputText) {
1631 return '';
1632 }
1633
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 }
1639
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 }
1755
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 }
1769
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 });
1775
1776 return processedText;
1777 }
1778
1779 function formatMarkdownHeaders(text) {
1780 // Handle h1 to h6 headers
1781 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1782 const level = hashes.length;
1783 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1784 });
1785 }
1786
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 function formatBoldText(text) {
1838 // This function is kept for compatibility but now uses formatTextStyling
1839 return formatTextStyling(text);
1840 }
1841
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);
1845
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 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>`;
1865 });
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>`;
1880 });
1881
1882 return text;
1883 }
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
1954 function sanitizeUserInput(text) {
1955 const div = document.createElement('div');
1956 div.textContent = text;
1957 return div.innerHTML;
1958 }
1959
1960 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>')) {
1964 return unsafe;
1965 }
1966
1967 return unsafe
1968 .replace(/&/g, "&amp;")
1969 .replace(/</g, "&lt;")
1970 .replace(/>/g, "&gt;")
1971 .replace(/"/g, "&quot;")
1972 .replace(/'/g, "&#039;");
1973 }
1974
1975 function decodeHTMLEntities(text) {
1976 var textArea = document.createElement('textarea');
1977 textArea.innerHTML = text;
1978 return textArea.value;
1979 }
1980
1981 // ====================================
1982 // UI & SCROLLING CONTROLS
1983 // ====================================
1984
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');
1998 if (instant) {
1999 // Instantly set the scroll position to the bottom
2000 chatBox.scrollTop(chatBox.prop("scrollHeight"));
2001 } else {
2002 // Use requestAnimationFrame for smoother scrolling if needed
2003 let start = null;
2004 const scrollHeight = chatBox.prop("scrollHeight");
2005 const initialScroll = chatBox.scrollTop();
2006 const distance = scrollHeight - initialScroll;
2007 const duration = 500; // Duration in ms
2008
2009 function smoothScroll(timestamp) {
2010 if (!start) start = timestamp;
2011 const progress = timestamp - start;
2012 const currentScroll = initialScroll + (distance * (progress / duration));
2013 chatBox.scrollTop(currentScroll);
2014
2015 if (progress < duration) {
2016 requestAnimationFrame(smoothScroll);
2017 } else {
2018 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2019 }
2020 }
2021
2022 requestAnimationFrame(smoothScroll);
2023 }
2024 }
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');
2030 var elementTop = element.position().top + chatBox.scrollTop();
2031 chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
2032 }
2033
2034 function showChatWidget(botId) {
2035 botId = botId || 'default';
2036 var $button = getElement(botId, 'floating-chatbot-button');
2037 // First ensure display is set
2038 $button.css('display', 'flex');
2039 // Then handle the fade
2040 $button.fadeTo(500, 1);
2041 // Force visibility
2042 $button.removeClass('hidden');
2043 }
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');
2050 }
2051
2052 function disableScroll() {
2053 if (isMobile()) {
2054 $('body').css('overflow', 'hidden');
2055 }
2056 }
2057
2058 function enableScroll() {
2059 if (isMobile()) {
2060 $('body').css('overflow', '');
2061 }
2062 }
2063
2064 function isMobile() {
2065 // This can be a simple check, or more sophisticated detection of mobile devices
2066 return window.innerWidth <= 768; // Example threshold for mobile devices
2067 }
2068
2069 function setFullHeight() {
2070 var vh = $(window).innerHeight() * 0.01;
2071 $(':root').css('--vh', vh + 'px');
2072 }
2073
2074
2075 // ====================================
2076 // NOTIFICATION SYSTEM
2077 // ====================================
2078
2079 function createNotificationBadge() {
2080 const chatButton = document.getElementById('floating-chatbot-button');
2081
2082 if (!chatButton) return;
2083
2084 // Remove any existing badge first
2085 const existingBadge = chatButton.querySelector('.chat-notification-badge');
2086 if (existingBadge) {
2087 existingBadge.remove();
2088 }
2089
2090 notificationBadge = document.createElement('div');
2091 notificationBadge.className = 'chat-notification-badge';
2092 notificationBadge.style.cssText = `
2093 display: none;
2094 position: absolute;
2095 top: -5px;
2096 right: -5px;
2097 background-color: red;
2098 color: white;
2099 border-radius: 50%;
2100 padding: 4px 8px;
2101 font-size: 12px;
2102 font-weight: bold;
2103 z-index: 10001;
2104 `;
2105 chatButton.style.position = 'relative';
2106 chatButton.appendChild(notificationBadge);
2107
2108 }
2109
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')) {
2115 badge.style.display = 'block';
2116 badge.textContent = '1';
2117 }
2118 }
2119
2120 function hideNotification(botId) {
2121 botId = botId || 'default';
2122 const badge = getElementDOM(botId, 'chat-notification-badge');
2123 if (badge) {
2124 badge.style.display = 'none';
2125 }
2126 }
2127
2128 function startNotificationChecking(botId) {
2129 botId = botId || 'default';
2130 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2131 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
2138 }
2139
2140 function stopNotificationChecking(botId) {
2141 botId = botId || 'default';
2142 var instance = MxChatInstances.get(botId);
2143 if (instance.notificationCheckInterval) {
2144 clearInterval(instance.notificationCheckInterval);
2145 }
2146 }
2147
2148 function checkForNewMessages() {
2149 const sessionId = getChatSession();
2150 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2151
2152 if (!chatPersistenceEnabled) return;
2153
2154 $.ajax({
2155 url: mxchatChat.ajax_url,
2156 type: 'POST',
2157 data: {
2158 action: 'mxchat_check_new_messages',
2159 session_id: sessionId,
2160 last_seen_id: lastSeenMessageId,
2161 nonce: mxchatChat.nonce
2162 },
2163 success: function(response) {
2164 if (response.success && response.data.hasNewMessages) {
2165 showNotification();
2166 }
2167 }
2168 });
2169 }
2170
2171
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;
2192 }
2193 }
2194
2195 function checkForAgentMessages(botId) {
2196 botId = botId || 'default';
2197 var instance = MxChatInstances.get(botId);
2198 const sessionId = getChatSession(botId);
2199 $.ajax({
2200 url: mxchatChat.ajax_url,
2201 type: 'POST',
2202 dataType: 'json',
2203 data: {
2204 action: 'mxchat_fetch_new_messages',
2205 session_id: sessionId,
2206 last_seen_id: instance.lastSeenMessageId,
2207 persistence_enabled: 'true',
2208 nonce: mxchatChat.nonce
2209 },
2210 success: function (response) {
2211 if (response.success && response.data?.new_messages) {
2212 let hasNewMessage = false;
2213
2214 response.data.new_messages.forEach(function (message) {
2215 if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
2216 hasNewMessage = true;
2217 appendMessage("agent", message.content, '', [], false, botId);
2218 instance.lastSeenMessageId = message.id;
2219 instance.processedMessageIds.add(message.id);
2220 }
2221 });
2222
2223 if (hasNewMessage) {
2224 enableChatInput(botId);
2225 }
2226
2227 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2228 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2229 showNotification(botId);
2230 }
2231
2232 scrollToBottom(botId, true);
2233 }
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 },
2240 error: function (xhr, status, error) {
2241 // Polling error - silently continue
2242 }
2243 });
2244 }
2245
2246 // ====================================
2247 // CHAT HISTORY & PERSISTENCE
2248 // ====================================
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');
2295 var $fragment = $(document.createDocumentFragment());
2296 let highestMessageId = instance.lastSeenMessageId;
2297
2298 // Update chat mode if provided
2299 if (response.data.chat_mode) {
2300 updateChatModeIndicator(response.data.chat_mode, botId);
2301 }
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
2364 if (message.id) {
2365 highestMessageId = Math.max(highestMessageId, message.id);
2366 instance.processedMessageIds.add(message.id);
2367 }
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);
2378 }
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 }
2394 }
2395 }
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();
2405 }
2406 }
2407
2408
2409 // ====================================
2410 // FILE UPLOAD FUNCTIONALITY
2411 // ====================================
2412
2413 function addSafeEventListener(elementId, eventType, handler) {
2414 const element = document.getElementById(elementId);
2415 if (element) {
2416 element.addEventListener(eventType, handler);
2417 }
2418 }
2419
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
2425 if (!container || !nameElement) {
2426 return;
2427 }
2428
2429 nameElement.textContent = filename;
2430 container.style.display = 'flex';
2431 }
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
2438 if (!container || !nameElement) {
2439 return;
2440 }
2441
2442 nameElement.textContent = filename;
2443 container.style.display = 'flex';
2444 }
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
2454 fetch(mxchatChat.ajax_url, {
2455 method: 'POST',
2456 headers: {
2457 'Content-Type': 'application/x-www-form-urlencoded',
2458 },
2459 body: new URLSearchParams({
2460 'action': 'mxchat_remove_pdf',
2461 'session_id': getChatSession(botId),
2462 'nonce': mxchatChat.nonce
2463 })
2464 })
2465 .then(response => response.json())
2466 .then(data => {
2467 if (data.success) {
2468 container.style.display = 'none';
2469 nameElement.textContent = '';
2470 activePdfFile = null;
2471 appendMessage('bot', 'PDF removed.');
2472 }
2473 })
2474 .catch(error => {
2475 // Error removing PDF - silently continue
2476 });
2477 }
2478
2479 function removeActiveWord() {
2480 const container = document.getElementById('active-word-container');
2481 const nameElement = document.getElementById('active-word-name');
2482
2483 if (!container || !nameElement || !activeWordFile) return;
2484
2485 fetch(mxchatChat.ajax_url, {
2486 method: 'POST',
2487 headers: {
2488 'Content-Type': 'application/x-www-form-urlencoded',
2489 },
2490 body: new URLSearchParams({
2491 'action': 'mxchat_remove_word',
2492 'session_id': sessionId,
2493 'nonce': mxchatChat.nonce
2494 })
2495 })
2496 .then(response => response.json())
2497 .then(data => {
2498 if (data.success) {
2499 container.style.display = 'none';
2500 nameElement.textContent = '';
2501 activeWordFile = null;
2502 appendMessage('bot', 'Word document removed.');
2503 }
2504 })
2505 .catch(error => {
2506 // Error removing Word document - silently continue
2507 });
2508 }
2509
2510 // ====================================
2511 // CONSENT & COMPLIANCE (GDPR)
2512 // ====================================
2513
2514 function initializeChatVisibility(botId) {
2515 botId = botId || 'default';
2516 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2517 mxchatChat.complianz_toggle === '1' ||
2518 mxchatChat.complianz_toggle === 1;
2519
2520 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2521 // Initial check
2522 checkConsentAndShowChat(botId);
2523
2524 // Listen for consent changes
2525 $(document).on('cmplz_status_change', function(event) {
2526 checkConsentAndShowChat(botId);
2527 });
2528 } else {
2529 // If Complianz is not enabled, always show
2530 getElement(botId, 'floating-chatbot-button')
2531 .css('display', 'flex')
2532 .removeClass('hidden no-consent')
2533 .fadeTo(500, 1);
2534
2535 // Also check pre-chat message when Complianz is not enabled
2536 checkPreChatDismissal(botId);
2537 }
2538 }
2539
2540
2541 function checkConsentAndShowChat(botId) {
2542 botId = botId || 'default';
2543 var consentStatus = cmplz_has_consent('marketing');
2544 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
2550 if (consentStatus === true) {
2551 $widget
2552 .removeClass('no-consent')
2553 .css('display', 'flex')
2554 .removeClass('hidden')
2555 .fadeTo(500, 1);
2556 $chatbot.removeClass('no-consent');
2557
2558 // Show pre-chat message if not dismissed
2559 checkPreChatDismissal(botId);
2560 } else {
2561 $widget
2562 .addClass('no-consent')
2563 .fadeTo(500, 0, function() {
2564 $(this)
2565 .css('display', 'none')
2566 .addClass('hidden');
2567 });
2568 $chatbot.addClass('no-consent');
2569
2570 // Hide pre-chat message when no consent
2571 $preChat.hide();
2572 }
2573 }
2574
2575
2576 // ====================================
2577 // PRE-CHAT MESSAGE HANDLING
2578 // ====================================
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;
2590 }
2591 // Expired — clear and show again
2592 localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2593 }
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 }
2599 }
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 }
2609 }
2610
2611
2612 // ====================================
2613 // UTILITY FUNCTIONS
2614 // ====================================
2615
2616 function copyToClipboard(text) {
2617 var tempInput = $('<input>');
2618 $('body').append(tempInput);
2619 tempInput.val(text).select();
2620 document.execCommand('copy');
2621 tempInput.remove();
2622 }
2623
2624
2625 function isImageHtml(str) {
2626 return str.startsWith('<img') && str.endsWith('>');
2627 }
2628
2629
2630 // ====================================
2631 // EVENT HANDLERS & INITIALIZATION
2632 // ====================================
2633
2634 $(document).on('click', '.mxchat-popular-question', function () {
2635 var question = $(this).text();
2636 var botId = getBotIdFromElement(this);
2637
2638 // Append the question as if the user typed it
2639 appendMessage("user", question, '', [], false, botId);
2640
2641 // Only collapse if there are questions
2642 if (hasQuickQuestions(botId)) {
2643 collapseQuickQuestions(botId);
2644 }
2645
2646 // Send the question to the server
2647 sendMessageToChatbot(question, botId);
2648 });
2649
2650 $(document).on('click', '.questions-toggle-btn', function(e) {
2651 e.preventDefault();
2652 e.stopPropagation();
2653 var botId = getBotIdFromElement(this);
2654 expandQuickQuestions(botId);
2655 });
2656
2657 $(document).on('click', '.questions-collapse-btn', function(e) {
2658 e.preventDefault();
2659 e.stopPropagation();
2660 var botId = getBotIdFromElement(this);
2661 collapseQuickQuestions(botId);
2662 });
2663
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');
2673 $(this).addClass('hidden');
2674 $badge.hide(); // Hide notification when opening chat
2675 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 }
2695 } else {
2696 $chatbot.removeClass('visible').addClass('hidden');
2697 $(this).removeClass('hidden');
2698 enableScroll();
2699 checkPreChatDismissal(botId);
2700 }
2701 });
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');
2708 enableScroll();
2709 });
2710
2711 $(document).on('click', '.close-pre-chat-message', function(e) {
2712 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2713 var botId = getBotIdFromElement(this);
2714 handlePreChatDismissal(botId);
2715 });
2716
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 });
2731
2732 // PDF file input change handler
2733 addSafeEventListener('pdf-upload', 'change', async function(e) {
2734 const file = e.target.files[0];
2735
2736 if (!file || file.type !== 'application/pdf') {
2737 alert('Please select a valid PDF file.');
2738 return;
2739 }
2740
2741 if (!sessionId) {
2742 alert('Error: No session ID found');
2743 return;
2744 }
2745
2746 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2747 alert('Error: Ajax configuration missing');
2748 return;
2749 }
2750
2751 // Disable buttons and show loading state
2752 const uploadBtn = document.getElementById('pdf-upload-btn');
2753 const sendBtn = document.getElementById('send-button');
2754 const originalBtnContent = uploadBtn.innerHTML;
2755
2756 try {
2757 const formData = new FormData();
2758 formData.append('action', 'mxchat_upload_pdf');
2759 formData.append('pdf_file', file);
2760 formData.append('session_id', sessionId);
2761 formData.append('nonce', mxchatChat.nonce);
2762
2763 uploadBtn.disabled = true;
2764 sendBtn.disabled = true;
2765 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2766 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2767 </svg>`;
2768
2769 const response = await fetch(mxchatChat.ajax_url, {
2770 method: 'POST',
2771 body: formData
2772 });
2773
2774 const data = await response.json();
2775
2776 if (data.success) {
2777 // Hide popular questions if they exist
2778 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2779 if (hasQuickQuestions()) {
2780 collapseQuickQuestions();
2781 }
2782
2783 // Show the active PDF name
2784 showActivePdf(data.data.filename);
2785
2786 appendMessage('bot', data.data.message);
2787 scrollToBottom();
2788 activePdfFile = data.data.filename;
2789 } else {
2790 alert('Failed to upload PDF. Please try again.');
2791 }
2792 } catch (error) {
2793 alert('Error uploading file. Please try again.');
2794 } finally {
2795 uploadBtn.disabled = false;
2796 sendBtn.disabled = false;
2797 uploadBtn.innerHTML = originalBtnContent;
2798 this.value = ''; // Reset file input
2799 }
2800 });
2801
2802 // Word file input change handler
2803 addSafeEventListener('word-upload', 'change', async function(e) {
2804 const file = e.target.files[0];
2805
2806 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2807 alert('Please select a valid Word document (.docx).');
2808 return;
2809 }
2810
2811 if (!sessionId) {
2812 alert('Error: No session ID found');
2813 return;
2814 }
2815
2816 // Disable buttons and show loading state
2817 const uploadBtn = document.getElementById('word-upload-btn');
2818 const sendBtn = document.getElementById('send-button');
2819 const originalBtnContent = uploadBtn.innerHTML;
2820
2821 try {
2822 const formData = new FormData();
2823 formData.append('action', 'mxchat_upload_word');
2824 formData.append('word_file', file);
2825 formData.append('session_id', sessionId);
2826 formData.append('nonce', mxchatChat.nonce);
2827
2828 uploadBtn.disabled = true;
2829 sendBtn.disabled = true;
2830 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2831 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2832 </svg>`;
2833
2834 const response = await fetch(mxchatChat.ajax_url, {
2835 method: 'POST',
2836 body: formData
2837 });
2838
2839 const data = await response.json();
2840
2841 if (data.success) {
2842 // Hide popular questions if they exist
2843 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2844 if (hasQuickQuestions()) {
2845 collapseQuickQuestions();
2846 }
2847
2848 // Show the active Word document name
2849 showActiveWord(data.data.filename);
2850
2851 appendMessage('bot', data.data.message);
2852 scrollToBottom();
2853 activeWordFile = data.data.filename;
2854 } else {
2855 alert('Failed to upload Word document. Please try again.');
2856 }
2857 } catch (error) {
2858 alert('Error uploading file. Please try again.');
2859 } finally {
2860 uploadBtn.disabled = false;
2861 sendBtn.disabled = false;
2862 uploadBtn.innerHTML = originalBtnContent;
2863 this.value = ''; // Reset file input
2864 }
2865 });
2866
2867 // Remove button click handlers
2868 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2869 e.preventDefault();
2870 e.stopPropagation();
2871 removeActivePdf();
2872 });
2873
2874 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2875 e.preventDefault();
2876 e.stopPropagation();
2877 removeActiveWord();
2878 });
2879
2880 // Window resize handlers
2881 $(window).on('resize orientationchange', function() {
2882 setFullHeight();
2883 });
2884
2885
2886 // ====================================
2887 // TOOLBAR & STYLING SETUP
2888 // ====================================
2889
2890 // Apply toolbar settings
2891 if (mxchatChat.chat_toolbar_toggle === 'on') {
2892 $('.chat-toolbar').show();
2893 } else {
2894 $('.chat-toolbar').hide();
2895 }
2896
2897 // Apply toolbar icon colors
2898 const toolbarElements = [
2899 '#mxchat-chatbot .toolbar-btn svg',
2900 '#mxchat-chatbot .active-pdf-name',
2901 '#mxchat-chatbot .active-word-name',
2902 '#mxchat-chatbot .remove-pdf-btn svg',
2903 '#mxchat-chatbot .remove-word-btn svg',
2904 '#mxchat-chatbot .toolbar-perplexity svg'
2905 ];
2906
2907 toolbarElements.forEach(selector => {
2908 $(selector).css({
2909 'fill': toolbarIconColor,
2910 'stroke': toolbarIconColor,
2911 'color': toolbarIconColor
2912 });
2913 });
2914
2915
2916 // ====================================
2917 // INIT LOADER & CHAT CONTAINER HELPERS
2918 // ====================================
2919 // These must be outside the email collection block so they're always available
2920 // (used by persistence loading even when email collection is off)
2921
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 });
2958 } else {
2959 hideInitLoader(botId);
2960 if (chatContainer) chatContainer.style.display = 'flex';
2961 if (typeof loadChatHistory === 'function') {
2962 loadChatHistory(botId);
2963 }
2964 }
2965 }
2966
2967 // ====================================
2968 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2969 // ====================================
2970 // Only run email collection setup if it's enabled
2971 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2972
2973 // Track submitting state per bot
2974 const emailSubmittingState = {};
2975
2976 // Add CSS animations for email form (once globally)
2977 if (!document.getElementById('email-error-styles')) {
2978 const style = document.createElement('style');
2979 style.id = 'email-error-styles';
2980 style.textContent = `
2981 @keyframes fadeInError {
2982 from { opacity: 0; transform: translateY(-5px); }
2983 to { opacity: 1; transform: translateY(0); }
2984 }
2985 .email-input-shake {
2986 animation: shake 0.5s ease-in-out;
2987 }
2988 @keyframes shake {
2989 0%, 100% { transform: translateX(0); }
2990 25% { transform: translateX(-5px); }
2991 75% { transform: translateX(5px); }
2992 }
2993 @keyframes spin {
2994 from { transform: rotate(0deg); }
2995 to { transform: rotate(360deg); }
2996 }
2997 .email-spinner {
2998 display: inline-block;
2999 vertical-align: middle;
3000 }
3001 `;
3002 document.head.appendChild(style);
3003 }
3004
3005 function isValidEmailAddress(email) {
3006 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3007 return emailRegex.test(email.trim()) && email.length <= 254;
3008 }
3009
3010 function isValidNameInput(name) {
3011 return name && name.trim().length >= 2 && name.trim().length <= 100;
3012 }
3013
3014 /**
3015 * Replace {visitor_name} placeholder in intro message with actual visitor name
3016 * @param {string} botId - The bot instance ID
3017 * @param {string} visitorName - The visitor's name to insert
3018 */
3019 function replaceVisitorNamePlaceholder(botId, visitorName) {
3020 var chatBox = getElementDOM(botId, 'chat-box');
3021 if (!chatBox) return;
3022
3023 // Find the first bot message (intro message)
3024 var introMessage = chatBox.querySelector('.bot-message');
3025 if (!introMessage) return;
3026
3027 var messageContent = introMessage.querySelector('div[dir="auto"]');
3028 if (!messageContent) return;
3029
3030 var html = messageContent.innerHTML;
3031
3032 // Replace {visitor_name} placeholder (case-insensitive)
3033 if (visitorName && visitorName.trim()) {
3034 // Escape HTML to prevent XSS
3035 var safeName = $('<div>').text(visitorName.trim()).html();
3036 html = html.replace(/\{visitor_name\}/gi, safeName);
3037 } else {
3038 // Remove placeholder and clean up spacing if no name provided
3039 html = html.replace(/\{visitor_name\}/gi, '');
3040 // Clean up any double spaces that might result
3041 html = html.replace(/\s{2,}/g, ' ').trim();
3042 }
3043
3044 messageContent.innerHTML = html;
3045 }
3046
3047 function setEmailSubmissionState(botId, loading) {
3048 var submitButton = getElementDOM(botId, 'email-submit-button');
3049 var emailInput = getElementDOM(botId, 'user-email');
3050 var nameInput = getElementDOM(botId, 'user-name');
3051
3052 if (loading) {
3053 emailSubmittingState[botId] = true;
3054 if (submitButton) submitButton.disabled = true;
3055 if (emailInput) emailInput.disabled = true;
3056 if (nameInput) nameInput.disabled = true;
3057
3058 if (submitButton && !submitButton.getAttribute('data-original-html')) {
3059 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3060 const originalText = submitButton.textContent;
3061 submitButton.innerHTML = `
3062 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3063 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3064 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3065 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3066 </circle>
3067 </svg>
3068 ${originalText}
3069 `;
3070 submitButton.style.opacity = '0.8';
3071 }
3072 } else {
3073 emailSubmittingState[botId] = false;
3074 if (submitButton) submitButton.disabled = false;
3075 if (emailInput) emailInput.disabled = false;
3076 if (nameInput) nameInput.disabled = false;
3077
3078 if (submitButton) {
3079 const originalHtml = submitButton.getAttribute('data-original-html');
3080 if (originalHtml) {
3081 submitButton.innerHTML = originalHtml;
3082 }
3083 submitButton.style.opacity = '1';
3084 }
3085 }
3086 }
3087
3088 function showEmailError(botId, message) {
3089 clearEmailError(botId);
3090
3091 var emailForm = getElementDOM(botId, 'email-collection-form');
3092 if (!emailForm) return;
3093
3094 const errorDiv = document.createElement('div');
3095 errorDiv.className = 'email-error';
3096 errorDiv.style.cssText = `
3097 color: #e74c3c;
3098 font-size: 12px;
3099 margin-top: 8px;
3100 padding: 4px 0;
3101 animation: fadeInError 0.3s ease;
3102 `;
3103 errorDiv.textContent = message;
3104 emailForm.appendChild(errorDiv);
3105
3106 // Add shake animation to inputs
3107 var emailInput = getElementDOM(botId, 'user-email');
3108 var nameInput = getElementDOM(botId, 'user-name');
3109
3110 if (emailInput) {
3111 emailInput.classList.add('email-input-shake');
3112 setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3113 }
3114 if (nameInput) {
3115 nameInput.classList.add('email-input-shake');
3116 setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3117 }
3118 }
3119
3120 function clearEmailError(botId) {
3121 var emailForm = getElementDOM(botId, 'email-collection-form');
3122 if (emailForm) {
3123 const existingErrors = emailForm.querySelectorAll('.email-error');
3124 existingErrors.forEach(error => error.remove());
3125 }
3126 }
3127
3128 // Resolve email state using server-side data when available, AJAX fallback otherwise
3129 function resolveEmailState(botId) {
3130 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3131 if (mxchatChat.initial_email_state.show_email_form) {
3132 showEmailFormForBot(botId);
3133 } else {
3134 showChatContainerForBot(botId);
3135 }
3136 } else {
3137 checkSessionAndEmailForBot(botId);
3138 }
3139 }
3140
3141 function checkSessionAndEmailForBot(botId) {
3142 const sessionId = MxChatInstances.ensureSession(botId);
3143
3144 // Hide both panels while we check — show loader instead
3145 var emailBlocker = getElementDOM(botId, 'email-blocker');
3146 var chatContainer = getElementDOM(botId, 'chat-container');
3147 if (emailBlocker) emailBlocker.style.display = 'none';
3148 if (chatContainer) chatContainer.style.display = 'none';
3149 showInitLoader(botId);
3150
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}`);
3165 }
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);
3177 }
3178 })
3179 .catch((error) => {
3180 showEmailFormForBot(botId);
3181 });
3182 }
3183
3184 // Event delegation for email form submission
3185 $(document).on('submit', '.email-collection-form', function(e) {
3186 e.preventDefault();
3187 e.stopPropagation();
3188
3189 var botId = getBotIdFromElement(this);
3190
3191 // Prevent double submission
3192 if (emailSubmittingState[botId]) {
3193 return false;
3194 }
3195
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);
3201
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}`);
3244 }
3245 return response.json();
3246 })
3247 .then((data) => {
3248 setEmailSubmissionState(botId, false);
3249
3250 if (data.success) {
3251 showChatContainerForBot(botId);
3252
3253 // Replace {visitor_name} placeholder in intro message with actual name
3254 if (userName) {
3255 replaceVisitorNamePlaceholder(botId, userName);
3256 } else {
3257 // Remove placeholder if no name provided
3258 replaceVisitorNamePlaceholder(botId, '');
3259 }
3260
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);
3268 }
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.');
3276 });
3277
3278 return false;
3279 });
3280
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 }
3310 }
3311 });
3312
3313 // Handle Enter key in name input
3314 $(document).on('keypress', '.mxchat-name-input', function(e) {
3315 if (e.key === 'Enter') {
3316 e.preventDefault();
3317 var botId = getBotIdFromElement(this);
3318 if (!emailSubmittingState[botId]) {
3319 $(this).closest('.email-collection-form').submit();
3320 }
3321 }
3322 });
3323
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 }
3344 }
3345 });
3346 }
3347
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);
3356 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 }
3374 });
3375
3376 // Legacy duplicate close handler removed — handled by single event delegation above
3377
3378
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');
3384 return questionButtons.length > 0;
3385 }
3386
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)) {
3401 questionsContainer.classList.add('collapsed');
3402 questionsContainer.classList.add('has-been-collapsed');
3403 try {
3404 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3405 sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
3406 } catch (e) {
3407 // Ignore if sessionStorage is not available
3408 }
3409 }
3410 }
3411
3412 function expandQuickQuestions(botId) {
3413 botId = botId || 'default';
3414 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3415 if (questionsContainer && hasQuickQuestions(botId)) {
3416 questionsContainer.classList.remove('collapsed');
3417 try {
3418 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
3419 } catch (e) {
3420 // Ignore if sessionStorage is not available
3421 }
3422 }
3423 }
3424
3425 function checkQuickQuestionsState(botId) {
3426 botId = botId || 'default';
3427 if (!hasQuickQuestions(botId)) {
3428 return; // Don't do anything if no questions exist
3429 }
3430
3431 // Skip restoring collapsed state for embedded bots - they should always start expanded
3432 if (isEmbeddedBot(botId)) {
3433 return;
3434 }
3435
3436 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');
3441 if (questionsContainer) {
3442 if (hasBeenCollapsed === 'true') {
3443 questionsContainer.classList.add('has-been-collapsed');
3444 }
3445 if (isCollapsed === 'true') {
3446 questionsContainer.classList.add('collapsed');
3447 }
3448 }
3449 } catch (e) {
3450 // Ignore if sessionStorage is not available
3451 }
3452 }
3453
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) {
3457 const $link = $(this);
3458 const messageDiv = $link.closest('.bot-message, .agent-message');
3459
3460 // Only process bot/agent message links
3461 if (messageDiv.length > 0) {
3462 const originalHref = $link.attr('href');
3463
3464 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3465 e.preventDefault();
3466 e.stopPropagation();
3467
3468 // Mark as tracked
3469 $link.attr('data-tracked', 'true');
3470
3471 // Get bot ID from the chat box context
3472 var botId = getBotIdFromElement(this);
3473
3474 // Get message context from the message div
3475 const messageText = messageDiv.text().substring(0, 200);
3476
3477 $.ajax({
3478 url: mxchatChat.ajax_url,
3479 type: 'POST',
3480 data: {
3481 action: 'mxchat_track_url_click',
3482 session_id: getChatSession(botId),
3483 url: originalHref,
3484 message_context: messageText,
3485 nonce: mxchatChat.nonce
3486 },
3487 complete: function() {
3488 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3489 window.open(originalHref, '_blank');
3490 } else {
3491 window.location.href = originalHref;
3492 }
3493 }
3494 });
3495
3496 return false;
3497 }
3498 }
3499 });
3500
3501 // ====================================
3502 // MAIN INITIALIZATION
3503 // ====================================
3504
3505 // Initialize all chatbot instances on the page
3506 initializeAllInstances();
3507
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 });
3513
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 }); // End of jQuery ready
3546
3547
3548 // ====================================
3549 // GLOBAL EVENT LISTENERS (Outside jQuery)
3550 // ====================================
3551
3552 // Event listener for copy button (code blocks)
3553 document.addEventListener("click", (e) => {
3554 if (e.target.classList.contains("mxchat-copy-button")) {
3555 const copyButton = e.target;
3556 const codeBlock = copyButton
3557 .closest(".mxchat-code-block-container")
3558 .querySelector(".mxchat-code-block code");
3559
3560 if (codeBlock) {
3561 // Preserve formatting using innerText
3562 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
3563 copyButton.textContent = "Copied!";
3564 copyButton.setAttribute("aria-label", "Copied to clipboard");
3565
3566 setTimeout(() => {
3567 copyButton.textContent = "Copy";
3568 copyButton.setAttribute("aria-label", "Copy to clipboard");
3569 }, 2000);
3570 });
3571 }
3572 }
3573 });
3574
3575