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

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