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

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

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