PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.7
MxChat – AI Chatbot & Content Generation for WordPress v3.1.7
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.1.7, at js/chat-script.js

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