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

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