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

2,916 lines 112.2 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
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 to check if streaming is supported for the current model
944 function isStreamingSupported(model) {
945 if (!model) return false;
946
947 //console.log("Checking streaming support for model:", model); // Debug log
948
949 // Get the model prefix
950 const modelPrefix = model.split('-')[0].toLowerCase();
951
952 //console.log("Model prefix:", modelPrefix); // Debug log
953
954 // Support streaming for OpenAI, Claude, Grok, and DeepSeek models
955 const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude' || modelPrefix === 'grok' || modelPrefix === 'deepseek';
956
957 //console.log("Streaming supported:", isSupported); // Debug log
958
959 return isSupported;
960 }
961
962 // Update the event handlers to use the correct function names
963 $('#send-button').off('click').on('click', function() {
964 sendMessage(); // Use the updated sendMessage function
965 });
966
967 // Override enter key handler
968 $('#chat-input').off('keypress').on('keypress', function(e) {
969 if (e.which == 13 && !e.shiftKey) {
970 e.preventDefault();
971 sendMessage(); // Use the updated sendMessage function
972 }
973 });
974
975
976 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
977 try {
978 // Determine styles based on sender type
979 let messageClass, bgColor, fontColor;
980
981 if (sender === "user") {
982 messageClass = "user-message";
983 bgColor = userMessageBgColor;
984 fontColor = userMessageFontColor;
985 // Only sanitize user input
986 messageText = sanitizeUserInput(messageText);
987 } else if (sender === "agent") {
988 messageClass = "agent-message";
989 bgColor = liveAgentMessageBgColor;
990 fontColor = liveAgentMessageFontColor;
991 } else {
992 messageClass = "bot-message";
993 bgColor = botMessageBgColor;
994 fontColor = botMessageFontColor;
995 }
996
997 const messageDiv = $('<div>')
998 .addClass(messageClass)
999 .attr('dir', 'auto')
1000 .css({
1001 'background': bgColor,
1002 'color': fontColor,
1003 'margin-bottom': '1em'
1004 });
1005
1006 // Process the message content based on sender
1007 let fullMessage;
1008 if (sender === "user") {
1009 // For user messages, apply linkify after sanitization
1010 fullMessage = linkify(messageText);
1011 } else {
1012 // For bot/agent messages, preserve HTML
1013 fullMessage = messageText;
1014 }
1015
1016 // Add images if provided
1017 if (images && images.length > 0) {
1018 fullMessage += '<div class="image-gallery" dir="auto">';
1019 images.forEach(img => {
1020 const safeTitle = sanitizeUserInput(img.title);
1021 const safeUrl = encodeURI(img.image_url);
1022 const safeThumbnail = encodeURI(img.thumbnail_url);
1023
1024 fullMessage += `
1025 <div style="margin-bottom: 10px;">
1026 <strong>${safeTitle}</strong><br>
1027 <a href="${safeUrl}" target="_blank">
1028 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
1029 </a>
1030 </div>`;
1031 });
1032 fullMessage += '</div>';
1033 }
1034
1035 // Append HTML content if provided
1036 if (messageHtml && sender !== "user") {
1037 fullMessage += '<br><br>' + messageHtml;
1038 }
1039
1040 messageDiv.html(fullMessage);
1041
1042 if (isTemporary) {
1043 messageDiv.addClass('temporary-message');
1044 }
1045
1046 messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1047 // FIXED: Use event delegation for link tracking
1048 if (sender === "bot" || sender === "agent") {
1049 attachLinkTracking(messageDiv, messageText);
1050 }
1051
1052 if (sender === "bot") {
1053 const lastUserMessage = $('#chat-box').find('.user-message').last();
1054 if (lastUserMessage.length) {
1055 scrollElementToTop(lastUserMessage);
1056 }
1057 }
1058 });
1059
1060 if (messageText.id) {
1061 lastSeenMessageId = messageText.id;
1062 hideNotification();
1063 }
1064 } catch (error) {
1065 console.error("Error rendering message:", error);
1066 }
1067 }
1068
1069 // Helper function to attach link tracking with proper event handling
1070 function attachLinkTracking(messageDiv, messageText) {
1071 // Use a slight delay to ensure DOM is ready
1072 setTimeout(function() {
1073 const links = messageDiv.find('a[href]').not('[data-tracked]');
1074
1075 links.each(function() {
1076 const $link = $(this);
1077 const originalHref = $link.attr('href');
1078
1079 // Mark as tracked to avoid duplicate handlers
1080 $link.attr('data-tracked', 'true');
1081
1082 // Only track external URLs
1083 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1084 // Remove any existing click handlers first
1085 $link.off('click.tracking');
1086
1087 // Add new click handler with namespace
1088 $link.on('click.tracking', function(e) {
1089 e.preventDefault();
1090 e.stopPropagation();
1091
1092 const messageContext = typeof messageText === 'string'
1093 ? messageText.substring(0, 200)
1094 : '';
1095
1096 // Track the click
1097 $.ajax({
1098 url: mxchatChat.ajax_url,
1099 type: 'POST',
1100 data: {
1101 action: 'mxchat_track_url_click',
1102 session_id: getChatSession(),
1103 url: originalHref,
1104 message_context: messageContext,
1105 nonce: mxchatChat.nonce
1106 },
1107 complete: function() {
1108 // Always redirect, even if tracking fails
1109 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1110 window.open(originalHref, '_blank');
1111 } else {
1112 window.location.href = originalHref;
1113 }
1114 }
1115 });
1116
1117 return false; // Extra insurance to prevent default
1118 });
1119 }
1120 });
1121 }, 100); // Small delay to ensure DOM is ready
1122 }
1123
1124 function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1125 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1126 var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1127
1128 // Determine styles
1129 let bgColor, fontColor;
1130 if (sender === "user") {
1131 bgColor = userMessageBgColor;
1132 fontColor = userMessageFontColor;
1133 } else if (sender === "agent") {
1134 bgColor = liveAgentMessageBgColor;
1135 fontColor = liveAgentMessageFontColor;
1136 } else {
1137 bgColor = botMessageBgColor;
1138 fontColor = botMessageFontColor;
1139 }
1140
1141 var fullMessage = linkify(responseText);
1142 if (responseHtml) {
1143 fullMessage += '<br><br>' + responseHtml;
1144 }
1145
1146 if (images.length > 0) {
1147 fullMessage += '<div class="image-gallery" dir="auto">';
1148 images.forEach(img => {
1149 fullMessage += `
1150 <div style="margin-bottom: 10px;">
1151 <strong>${img.title}</strong><br>
1152 <a href="${img.image_url}" target="_blank">
1153 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1154 </a>
1155 </div>`;
1156 });
1157 fullMessage += '</div>';
1158 }
1159
1160 if (lastMessageDiv.length) {
1161 lastMessageDiv.fadeOut(200, function() {
1162 $(this)
1163 .html(fullMessage)
1164 .removeClass('bot-message user-message')
1165 .addClass(messageClass)
1166 .attr('dir', 'auto')
1167 .css({
1168 'background-color': bgColor,
1169 'color': fontColor,
1170 })
1171 .removeClass('temporary-message')
1172 .fadeIn(200, function() {
1173 // FIXED: Use the helper function for link tracking
1174 if (sender === "bot" || sender === "agent") {
1175 attachLinkTracking($(this), responseText);
1176 }
1177
1178 if (sender === "bot" || sender === "agent") {
1179 const lastUserMessage = $('#chat-box').find('.user-message').last();
1180 if (lastUserMessage.length) {
1181 scrollElementToTop(lastUserMessage);
1182 }
1183 // Show notification if chat is hidden
1184 if ($('#floating-chatbot').hasClass('hidden')) {
1185 showNotification();
1186 }
1187 }
1188 });
1189 });
1190 } else {
1191 appendMessage(sender, responseText, responseHtml, images);
1192 }
1193 }
1194
1195
1196 function appendThinkingMessage() {
1197 // Remove any existing thinking dots first
1198 $('.thinking-dots').remove();
1199
1200 // Retrieve the bot message font color and background color
1201 var botMessageFontColor = mxchatChat.bot_message_font_color;
1202 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1203
1204
1205 var thinkingHtml = '<div class="thinking-dots-container">' +
1206 '<div class="thinking-dots">' +
1207 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1208 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1209 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1210 '</div>' +
1211 '</div>';
1212
1213 // Append the thinking dots to the chat container (or within the temporary message div)
1214 $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1215 scrollToBottom();
1216 }
1217
1218 function removeThinkingDots() {
1219 $('.thinking-dots').closest('.temporary-message').remove();
1220 }
1221
1222 // ====================================
1223 // TEXT FORMATTING & PROCESSING
1224 // ====================================
1225
1226 function linkify(inputText) {
1227 //console.log('=== LINKIFY DEBUG START ===');
1228 //console.log('Input to linkify:', inputText);
1229 //console.log('Input type:', typeof inputText);
1230 //console.log('Input length:', inputText ? inputText.length : 'null/undefined');
1231
1232 if (!inputText) {
1233 //console.log('Empty input, returning empty string');
1234 return '';
1235 }
1236
1237 // Process markdown headers FIRST (before paragraph wrapping)
1238 //console.log('--- Processing Headers ---');
1239 let processedText = formatMarkdownHeaders(inputText);
1240 //console.log('After headers:', processedText);
1241 //console.log('Headers changed:', processedText !== inputText);
1242
1243 // Process text styling (bold, italic, strikethrough)
1244 //console.log('--- Processing Text Styling ---');
1245 //const beforeStyling = processedText;
1246 processedText = formatTextStyling(processedText);
1247 //console.log('After styling:', processedText);
1248 //console.log('Styling changed:', processedText !== beforeStyling);
1249
1250 // Process code blocks BEFORE processing links to avoid conflicts
1251 //console.log('--- Processing Code Blocks ---');
1252 const beforeCodeBlocks = processedText;
1253 processedText = formatCodeBlocks(processedText);
1254 //console.log('After code blocks:', processedText);
1255 //console.log('Code blocks changed:', processedText !== beforeCodeBlocks);
1256
1257 // NOW convert to paragraphs (this should be AFTER markdown processing)
1258 //console.log('--- Converting to Paragraphs ---');
1259 const beforeParagraphs = processedText;
1260 processedText = convertNewlinesToBreaks(processedText);
1261 //console.log('After paragraph conversion:', processedText);
1262 //console.log('Paragraphs changed:', processedText !== beforeParagraphs);
1263
1264 // Process markdown links
1265 //console.log('--- Processing Markdown Links ---');
1266 const beforeMarkdownLinks = processedText;
1267 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1268 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1269 //console.log('Found markdown link:', match, 'text:', text, 'url:', url);
1270 const safeUrl = encodeURI(url);
1271 const safeText = sanitizeUserInput(text);
1272 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1273 });
1274 //console.log('After markdown links:', processedText);
1275 //console.log('Markdown links changed:', processedText !== beforeMarkdownLinks);
1276
1277 // Process phone numbers (tel:)
1278 //console.log('--- Processing Phone Numbers ---');
1279 const beforePhone = processedText;
1280 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1281 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1282 //console.log('Found phone link:', match, 'text:', text, 'phone:', phone);
1283 const safePhone = encodeURI(phone);
1284 const safeText = sanitizeUserInput(text);
1285 return `<a href="${safePhone}">${safeText}</a>`;
1286 });
1287 //console.log('After phone numbers:', processedText);
1288 //console.log('Phone numbers changed:', processedText !== beforePhone);
1289
1290 // Process mailto links
1291 //console.log('--- Processing Mailto Links ---');
1292 const beforeMailto = processedText;
1293 const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1294 processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1295 console.log('Found mailto link:', match, 'text:', text, 'mailto:', mailto);
1296 const safeMailto = encodeURI(mailto);
1297 const safeText = sanitizeUserInput(text);
1298 return `<a href="${safeMailto}">${safeText}</a>`;
1299 });
1300 //console.log('After mailto links:', processedText);
1301 //console.log('Mailto links changed:', processedText !== beforeMailto);
1302
1303 // Process standalone URLs (avoid already processed links)
1304 //console.log('--- Processing Standalone URLs ---');
1305 const beforeStandaloneUrls = processedText;
1306 const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/gim;
1307 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1308 //console.log('Found standalone URL:', match, 'prefix:', prefix, 'url:', url);
1309 const safeUrl = encodeURI(url);
1310 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
1311 });
1312 //console.log('After standalone URLs:', processedText);
1313 //console.log('Standalone URLs changed:', processedText !== beforeStandaloneUrls);
1314
1315 // Process www. URLs (avoid already processed links)
1316 //console.log('--- Processing WWW URLs ---');
1317 const beforeWwwUrls = processedText;
1318 const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
1319 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1320 //console.log('Found www URL:', match, 'prefix:', prefix, 'url:', url);
1321 const safeUrl = encodeURI(`http://${url}`);
1322 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
1323 });
1324 //console.log('After www URLs:', processedText);
1325 //console.log('WWW URLs changed:', processedText !== beforeWwwUrls);
1326
1327 //console.log('=== FINAL RESULT ===');
1328 //console.log('Final processed text:', processedText);
1329 //console.log('Total transformation:', inputText !== processedText);
1330 //console.log('=== LINKIFY DEBUG END ===');
1331
1332 return processedText;
1333 }
1334
1335 function formatMarkdownHeaders(text) {
1336 // Handle h1 to h6 headers
1337 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1338 const level = hashes.length;
1339 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1340 });
1341 }
1342
1343 function formatTextStyling(text) {
1344 // Handle bold text (**text**)
1345 text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1346
1347 // Handle italic text (*text* or _text_) - avoid conflicts with bold
1348 text = text.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1349 text = text.replace(/(?<!_)_([^_\n]+)_(?!_)/g, '<em>$1</em>');
1350
1351 // Handle strikethrough (~~text~~)
1352 text = text.replace(/~~(.*?)~~/g, '<del>$1</del>');
1353
1354 return text;
1355 }
1356
1357 function formatBoldText(text) {
1358 // This function is kept for compatibility but now uses formatTextStyling
1359 return formatTextStyling(text);
1360 }
1361
1362 function convertNewlinesToBreaks(text) {
1363 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1364 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1365
1366 // Filter out empty paragraphs and wrap each paragraph in <p> tags
1367 return paragraphs
1368 .map(para => para.trim())
1369 .filter(para => para.length > 0) // Remove empty paragraphs
1370 .map(para => `<p>${para}</p>`)
1371 .join('');
1372 }
1373 function formatCodeBlocks(text) {
1374 // Handle fenced code blocks with language specification (```language)
1375 text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1376 const lang = language || 'text';
1377 const escapedCode = escapeHtml(code.trim());
1378 return `<div class="mxchat-code-block-container">
1379 <div class="mxchat-code-header">
1380 <span class="mxchat-code-language">${lang}</span>
1381 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1382 </div>
1383 <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1384 </div>`;
1385 });
1386
1387 // Handle inline code with single backticks
1388 text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1389
1390 // Handle raw PHP tags (legacy support)
1391 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1392 const escapedCode = escapeHtml(match);
1393 return `<div class="mxchat-code-block-container">
1394 <div class="mxchat-code-header">
1395 <span class="mxchat-code-language">php</span>
1396 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1397 </div>
1398 <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1399 </div>`;
1400 });
1401
1402 return text;
1403 }
1404
1405 function sanitizeUserInput(text) {
1406 const div = document.createElement('div');
1407 div.textContent = text;
1408 return div.innerHTML;
1409 }
1410
1411 function escapeHtml(unsafe) {
1412 // Skip escaping if it's already escaped or contains HTML code block markup
1413 if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
1414 unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1415 return unsafe;
1416 }
1417
1418 return unsafe
1419 .replace(/&/g, "&amp;")
1420 .replace(/</g, "&lt;")
1421 .replace(/>/g, "&gt;")
1422 .replace(/"/g, "&quot;")
1423 .replace(/'/g, "&#039;");
1424 }
1425
1426 function decodeHTMLEntities(text) {
1427 var textArea = document.createElement('textarea');
1428 textArea.innerHTML = text;
1429 return textArea.value;
1430 }
1431
1432 // ====================================
1433 // UI & SCROLLING CONTROLS
1434 // ====================================
1435
1436 function scrollToBottom(instant = false) {
1437 var chatBox = $('#chat-box');
1438 if (instant) {
1439 // Instantly set the scroll position to the bottom
1440 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1441 } else {
1442 // Use requestAnimationFrame for smoother scrolling if needed
1443 let start = null;
1444 const scrollHeight = chatBox.prop("scrollHeight");
1445 const initialScroll = chatBox.scrollTop();
1446 const distance = scrollHeight - initialScroll;
1447 const duration = 500; // Duration in ms
1448
1449 function smoothScroll(timestamp) {
1450 if (!start) start = timestamp;
1451 const progress = timestamp - start;
1452 const currentScroll = initialScroll + (distance * (progress / duration));
1453 chatBox.scrollTop(currentScroll);
1454
1455 if (progress < duration) {
1456 requestAnimationFrame(smoothScroll);
1457 } else {
1458 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1459 }
1460 }
1461
1462 requestAnimationFrame(smoothScroll);
1463 }
1464 }
1465
1466 function scrollElementToTop(element) {
1467 var chatBox = $('#chat-box');
1468 var elementTop = element.position().top + chatBox.scrollTop();
1469 chatBox.animate({ scrollTop: elementTop }, 500);
1470 }
1471
1472 function showChatWidget() {
1473 // First ensure display is set
1474 $('#floating-chatbot-button').css('display', 'flex');
1475 // Then handle the fade
1476 $('#floating-chatbot-button').fadeTo(500, 1);
1477 // Force visibility
1478 $('#floating-chatbot-button').removeClass('hidden');
1479 //console.log('Showing widget');
1480 }
1481
1482 function hideChatWidget() {
1483 $('#floating-chatbot-button').css('display', 'none');
1484 $('#floating-chatbot-button').addClass('hidden');
1485 //console.log('Hiding widget');
1486 }
1487
1488 function disableScroll() {
1489 if (isMobile()) {
1490 $('body').css('overflow', 'hidden');
1491 }
1492 }
1493
1494 function enableScroll() {
1495 if (isMobile()) {
1496 $('body').css('overflow', '');
1497 }
1498 }
1499
1500 function isMobile() {
1501 // This can be a simple check, or more sophisticated detection of mobile devices
1502 return window.innerWidth <= 768; // Example threshold for mobile devices
1503 }
1504
1505 function setFullHeight() {
1506 var vh = $(window).innerHeight() * 0.01;
1507 $(':root').css('--vh', vh + 'px');
1508 }
1509
1510
1511 // ====================================
1512 // NOTIFICATION SYSTEM
1513 // ====================================
1514
1515 function createNotificationBadge() {
1516 //console.log("Creating notification badge...");
1517 const chatButton = document.getElementById('floating-chatbot-button');
1518 //console.log("Chat button found:", !!chatButton);
1519
1520 if (!chatButton) return;
1521
1522 // Remove any existing badge first
1523 const existingBadge = chatButton.querySelector('.chat-notification-badge');
1524 if (existingBadge) {
1525 //console.log("Removing existing badge");
1526 existingBadge.remove();
1527 }
1528
1529 notificationBadge = document.createElement('div');
1530 notificationBadge.className = 'chat-notification-badge';
1531 notificationBadge.style.cssText = `
1532 display: none;
1533 position: absolute;
1534 top: -5px;
1535 right: -5px;
1536 background-color: red;
1537 color: white;
1538 border-radius: 50%;
1539 padding: 4px 8px;
1540 font-size: 12px;
1541 font-weight: bold;
1542 z-index: 10001;
1543 `;
1544 chatButton.style.position = 'relative';
1545 chatButton.appendChild(notificationBadge);
1546
1547 }
1548
1549 function showNotification() {
1550 const badge = document.getElementById('chat-notification-badge');
1551 if (badge && $('#floating-chatbot').hasClass('hidden')) {
1552 badge.style.display = 'block';
1553 badge.textContent = '1';
1554 }
1555 }
1556
1557 function hideNotification() {
1558 const badge = document.getElementById('chat-notification-badge');
1559 if (badge) {
1560 badge.style.display = 'none';
1561 }
1562 }
1563
1564 function startNotificationChecking() {
1565 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1566 if (!chatPersistenceEnabled) return;
1567
1568 createNotificationBadge();
1569 notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
1570 }
1571
1572 function stopNotificationChecking() {
1573 if (notificationCheckInterval) {
1574 clearInterval(notificationCheckInterval);
1575 }
1576 }
1577
1578 function checkForNewMessages() {
1579 const sessionId = getChatSession();
1580 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1581
1582 if (!chatPersistenceEnabled) return;
1583
1584 $.ajax({
1585 url: mxchatChat.ajax_url,
1586 type: 'POST',
1587 data: {
1588 action: 'mxchat_check_new_messages',
1589 session_id: sessionId,
1590 last_seen_id: lastSeenMessageId,
1591 nonce: mxchatChat.nonce
1592 },
1593 success: function(response) {
1594 if (response.success && response.data.hasNewMessages) {
1595 showNotification();
1596 }
1597 }
1598 });
1599 }
1600
1601
1602 // ====================================
1603 // LIVE AGENT FUNCTIONALITY
1604 // ====================================
1605
1606 function updateChatModeIndicator(mode) {
1607 //console.log("updateChatModeIndicator called with mode:", mode);
1608 const indicator = document.getElementById('chat-mode-indicator');
1609 if (indicator) {
1610 const oldText = indicator.textContent;
1611
1612 if (mode === 'agent') {
1613 indicator.textContent = 'Live Agent';
1614 startPolling();
1615 //console.log("Set indicator to Live Agent");
1616 } else {
1617 // Everything else is AI mode
1618 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1619 indicator.textContent = customAiText;
1620 stopPolling();
1621 //console.log("Set indicator to AI Agent:", customAiText);
1622 }
1623
1624 // Force immediate DOM update
1625 if (oldText !== indicator.textContent) {
1626 // Force a reflow to ensure the change is visible immediately
1627 indicator.offsetHeight;
1628
1629 // Double-check after a brief moment
1630 setTimeout(() => {
1631 //console.log("Final indicator text:", indicator.textContent);
1632 }, 50);
1633 }
1634 } else {
1635 console.error("Chat mode indicator element not found!");
1636 }
1637 }
1638
1639 function startPolling() {
1640 // Clear any existing interval first
1641 stopPolling();
1642 // Start new polling interval
1643 pollingInterval = setInterval(checkForAgentMessages, 5000);
1644 //console.log("Started agent message polling");
1645 }
1646
1647 function stopPolling() {
1648 if (pollingInterval) {
1649 clearInterval(pollingInterval);
1650 pollingInterval = null;
1651 //console.log("Stopped agent message polling");
1652 }
1653 }
1654
1655 function checkForAgentMessages() {
1656 const sessionId = getChatSession();
1657 $.ajax({
1658 url: mxchatChat.ajax_url,
1659 type: 'POST',
1660 dataType: 'json',
1661 data: {
1662 action: 'mxchat_fetch_new_messages',
1663 session_id: sessionId,
1664 last_seen_id: lastSeenMessageId,
1665 persistence_enabled: 'true', // Add this too
1666 nonce: mxchatChat.nonce
1667 },
1668 success: function (response) {
1669 if (response.success && response.data?.new_messages) {
1670 let hasNewMessage = false;
1671
1672 response.data.new_messages.forEach(function (message) {
1673 if (message.role === "agent" && !processedMessageIds.has(message.id)) {
1674 hasNewMessage = true;
1675 // CHANGE THIS LINE:
1676 appendMessage("agent", message.content); // Instead of replaceLastMessage
1677 lastSeenMessageId = message.id;
1678 processedMessageIds.add(message.id);
1679 }
1680 });
1681
1682 if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1683 showNotification();
1684 }
1685
1686 scrollToBottom(true);
1687 }
1688 },
1689 error: function (xhr, status, error) {
1690 //console.error("Polling error:", xhr, status, error);
1691 }
1692 });
1693 }
1694
1695 // ====================================
1696 // CHAT HISTORY & PERSISTENCE
1697 // ====================================
1698
1699 function loadChatHistory() {
1700 var sessionId = getChatSession();
1701 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1702
1703 if (chatPersistenceEnabled && sessionId) {
1704 $.ajax({
1705 url: mxchatChat.ajax_url,
1706 type: 'POST',
1707 dataType: 'json',
1708 data: {
1709 action: 'mxchat_fetch_conversation_history',
1710 session_id: sessionId
1711 },
1712 success: function(response) {
1713 // Check if the response indicates success
1714 if (response.success) {
1715 // Handle case where conversation data exists and is an array
1716 if (response.data && Array.isArray(response.data.conversation)) {
1717 var $chatBox = $('#chat-box');
1718 var $fragment = $(document.createDocumentFragment());
1719 let highestMessageId = lastSeenMessageId;
1720
1721 // Update chat mode if provided
1722 if (response.data.chat_mode) {
1723 updateChatModeIndicator(response.data.chat_mode);
1724 }
1725
1726 // Only process if there are actual messages
1727 if (response.data.conversation.length > 0) {
1728 $.each(response.data.conversation, function(index, message) {
1729 // Skip agent messages if persistence is off
1730 if (!chatPersistenceEnabled && message.role === 'agent') {
1731 return;
1732 }
1733
1734 var messageClass, messageBgColor, messageFontColor;
1735
1736 switch (message.role) {
1737 case 'user':
1738 messageClass = 'user-message';
1739 messageBgColor = userMessageBgColor;
1740 messageFontColor = userMessageFontColor;
1741 break;
1742 case 'agent':
1743 messageClass = 'agent-message';
1744 messageBgColor = liveAgentMessageBgColor;
1745 messageFontColor = liveAgentMessageFontColor;
1746 break;
1747 default:
1748 messageClass = 'bot-message';
1749 messageBgColor = botMessageBgColor;
1750 messageFontColor = botMessageFontColor;
1751 break;
1752 }
1753
1754 var messageElement = $('<div>').addClass(messageClass)
1755 .css({
1756 'background': messageBgColor,
1757 'color': messageFontColor
1758 });
1759
1760 var content = message.content;
1761 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1762 content = decodeHTMLEntities(content);
1763
1764 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1765 messageElement.html(content);
1766 } else {
1767 var formattedContent = linkify(content);
1768 messageElement.html(formattedContent);
1769 }
1770
1771 $fragment.append(messageElement);
1772
1773 // Track message IDs
1774 if (message.id) {
1775 highestMessageId = Math.max(highestMessageId, message.id);
1776 processedMessageIds.add(message.id);
1777 }
1778 });
1779
1780 // Only append messages and scroll if we have content
1781 $chatBox.append($fragment);
1782 scrollToBottom(true);
1783
1784 // Collapse quick questions if we have conversation history
1785 if (hasQuickQuestions()) {
1786 collapseQuickQuestions();
1787 }
1788
1789 // Update lastSeenMessageId after history loads
1790 lastSeenMessageId = highestMessageId;
1791
1792 // Only update chat mode if persistence is enabled and we have messages
1793 if (chatPersistenceEnabled) {
1794 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1795 if (lastMessage.role === 'agent') {
1796 updateChatModeIndicator('agent');
1797 }
1798 }
1799 } else {
1800 // No conversation history - this is normal for new chats
1801 //console.log("No conversation history found for this session - starting fresh chat.");
1802 }
1803 } else {
1804 // Response successful but no conversation data - this is also normal
1805 //console.log("No conversation data in response - starting fresh chat.");
1806 }
1807 } else {
1808 // Only show error if the response explicitly indicates an error
1809 console.warn("Failed to load chat history:", response.message || "Unknown error");
1810 // Don't show error message to user for failed history loads
1811 // Just start with a fresh chat instead
1812 }
1813 },
1814 error: function(xhr, status, error) {
1815 // Only log AJAX errors, don't show them to the user
1816 console.error("AJAX error loading chat history:", status, error);
1817 // Start with fresh chat - don't show error to user
1818 }
1819 });
1820 } else {
1821 //console.log("Chat persistence is disabled or no session ID found. Starting fresh chat.");
1822 }
1823 }
1824
1825
1826 // ====================================
1827 // FILE UPLOAD FUNCTIONALITY
1828 // ====================================
1829
1830 function addSafeEventListener(elementId, eventType, handler) {
1831 const element = document.getElementById(elementId);
1832 if (element) {
1833 element.addEventListener(eventType, handler);
1834 }
1835 }
1836
1837 function showActivePdf(filename) {
1838 const container = document.getElementById('active-pdf-container');
1839 const nameElement = document.getElementById('active-pdf-name');
1840
1841 if (!container || !nameElement) {
1842 //console.error('PDF container elements not found');
1843 return;
1844 }
1845
1846 nameElement.textContent = filename;
1847 container.style.display = 'flex';
1848 }
1849
1850 function showActiveWord(filename) {
1851 const container = document.getElementById('active-word-container');
1852 const nameElement = document.getElementById('active-word-name');
1853
1854 if (!container || !nameElement) {
1855 //console.error('Word document container elements not found');
1856 return;
1857 }
1858
1859 nameElement.textContent = filename;
1860 container.style.display = 'flex';
1861 }
1862
1863 function removeActivePdf() {
1864 const container = document.getElementById('active-pdf-container');
1865 const nameElement = document.getElementById('active-pdf-name');
1866
1867 if (!container || !nameElement || !activePdfFile) return;
1868
1869 fetch(mxchatChat.ajax_url, {
1870 method: 'POST',
1871 headers: {
1872 'Content-Type': 'application/x-www-form-urlencoded',
1873 },
1874 body: new URLSearchParams({
1875 'action': 'mxchat_remove_pdf',
1876 'session_id': sessionId,
1877 'nonce': mxchatChat.nonce
1878 })
1879 })
1880 .then(response => response.json())
1881 .then(data => {
1882 if (data.success) {
1883 container.style.display = 'none';
1884 nameElement.textContent = '';
1885 activePdfFile = null;
1886 appendMessage('bot', 'PDF removed.');
1887 }
1888 })
1889 .catch(error => {
1890 //console.error('Error removing PDF:', error);
1891 });
1892 }
1893
1894 function removeActiveWord() {
1895 const container = document.getElementById('active-word-container');
1896 const nameElement = document.getElementById('active-word-name');
1897
1898 if (!container || !nameElement || !activeWordFile) return;
1899
1900 fetch(mxchatChat.ajax_url, {
1901 method: 'POST',
1902 headers: {
1903 'Content-Type': 'application/x-www-form-urlencoded',
1904 },
1905 body: new URLSearchParams({
1906 'action': 'mxchat_remove_word',
1907 'session_id': sessionId,
1908 'nonce': mxchatChat.nonce
1909 })
1910 })
1911 .then(response => response.json())
1912 .then(data => {
1913 if (data.success) {
1914 container.style.display = 'none';
1915 nameElement.textContent = '';
1916 activeWordFile = null;
1917 appendMessage('bot', 'Word document removed.');
1918 }
1919 })
1920 .catch(error => {
1921 //console.error('Error removing Word document:', error);
1922 });
1923 }
1924
1925 // ====================================
1926 // CONSENT & COMPLIANCE (GDPR)
1927 // ====================================
1928
1929 function initializeChatVisibility() {
1930 //console.log('Initializing chat visibility');
1931 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1932 mxchatChat.complianz_toggle === '1' ||
1933 mxchatChat.complianz_toggle === 1;
1934
1935 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1936 // Initial check
1937 checkConsentAndShowChat();
1938
1939 // Listen for consent changes
1940 $(document).on('cmplz_status_change', function(event) {
1941 //console.log('Status change detected');
1942 checkConsentAndShowChat();
1943 });
1944 } else {
1945 // If Complianz is not enabled, always show
1946 $('#floating-chatbot-button')
1947 .css('display', 'flex')
1948 .removeClass('hidden no-consent')
1949 .fadeTo(500, 1);
1950
1951 // Also check pre-chat message when Complianz is not enabled
1952 checkPreChatDismissal();
1953 }
1954 }
1955
1956
1957 function checkConsentAndShowChat() {
1958 var consentStatus = cmplz_has_consent('marketing');
1959 var consentType = complianz.consenttype;
1960
1961 //console.log('Checking consent:', {status: consentStatus,type: consentType});
1962
1963 let $widget = $('#floating-chatbot-button');
1964 let $chatbot = $('#floating-chatbot');
1965 let $preChat = $('#pre-chat-message');
1966
1967 if (consentStatus === true) {
1968 //console.log('Consent granted - showing widget');
1969 $widget
1970 .removeClass('no-consent')
1971 .css('display', 'flex')
1972 .removeClass('hidden')
1973 .fadeTo(500, 1);
1974 $chatbot.removeClass('no-consent');
1975
1976 // Show pre-chat message if not dismissed
1977 checkPreChatDismissal();
1978 } else {
1979 //console.log('No consent - hiding widget');
1980 $widget
1981 .addClass('no-consent')
1982 .fadeTo(500, 0, function() {
1983 $(this)
1984 .css('display', 'none')
1985 .addClass('hidden');
1986 });
1987 $chatbot.addClass('no-consent');
1988
1989 // Hide pre-chat message when no consent
1990 $preChat.hide();
1991 }
1992 }
1993
1994
1995 // ====================================
1996 // PRE-CHAT MESSAGE HANDLING
1997 // ====================================
1998
1999 function checkPreChatDismissal() {
2000 $.ajax({
2001 url: mxchatChat.ajax_url,
2002 type: 'POST',
2003 data: {
2004 action: 'mxchat_check_pre_chat_message_status',
2005 _ajax_nonce: mxchatChat.nonce
2006 },
2007 success: function(response) {
2008 if (response.success && !response.data.dismissed) {
2009 $('#pre-chat-message').fadeIn(250);
2010 } else {
2011 $('#pre-chat-message').hide();
2012 }
2013 },
2014 error: function() {
2015 //console.error('Failed to check pre-chat message dismissal status.');
2016 }
2017 });
2018 }
2019
2020 function handlePreChatDismissal() {
2021 $('#pre-chat-message').fadeOut(200);
2022 $.ajax({
2023 url: mxchatChat.ajax_url,
2024 type: 'POST',
2025 data: {
2026 action: 'mxchat_dismiss_pre_chat_message',
2027 _ajax_nonce: mxchatChat.nonce
2028 },
2029 success: function() {
2030 $('#pre-chat-message').hide();
2031 },
2032 error: function() {
2033 //console.error('Failed to dismiss pre-chat message.');
2034 }
2035 });
2036 }
2037
2038
2039 // ====================================
2040 // UTILITY FUNCTIONS
2041 // ====================================
2042
2043 function copyToClipboard(text) {
2044 var tempInput = $('<input>');
2045 $('body').append(tempInput);
2046 tempInput.val(text).select();
2047 document.execCommand('copy');
2048 tempInput.remove();
2049 }
2050
2051
2052 function isImageHtml(str) {
2053 return str.startsWith('<img') && str.endsWith('>');
2054 }
2055
2056
2057 // ====================================
2058 // EVENT HANDLERS & INITIALIZATION
2059 // ====================================
2060
2061 $(document).on('click', '.mxchat-popular-question', function () {
2062 var question = $(this).text();
2063
2064 // Append the question as if the user typed it
2065 appendMessage("user", question);
2066
2067 // Only collapse if there are questions
2068 if (hasQuickQuestions()) {
2069 collapseQuickQuestions();
2070 }
2071
2072 // Send the question to the server
2073 sendMessageToChatbot(question);
2074 });
2075
2076 $(document).on('click', '.questions-toggle-btn', function(e) {
2077 e.preventDefault();
2078 e.stopPropagation();
2079 expandQuickQuestions();
2080 });
2081
2082 $(document).on('click', '.questions-collapse-btn', function(e) {
2083 e.preventDefault();
2084 e.stopPropagation();
2085 collapseQuickQuestions();
2086 });
2087
2088 // Chatbot visibility toggle handlers
2089 $(document).on('click', '#floating-chatbot-button', function() {
2090 var chatbot = $('#floating-chatbot');
2091 if (chatbot.hasClass('hidden')) {
2092 chatbot.removeClass('hidden').addClass('visible');
2093 $(this).addClass('hidden');
2094 $('#chat-notification-badge').hide(); // Hide notification when opening chat
2095 disableScroll();
2096 $('#pre-chat-message').fadeOut(250);
2097 } else {
2098 chatbot.removeClass('visible').addClass('hidden');
2099 $(this).removeClass('hidden');
2100 enableScroll();
2101 checkPreChatDismissal();
2102 }
2103 });
2104
2105 $(document).on('click', '#exit-chat-button', function() {
2106 $('#floating-chatbot').addClass('hidden').removeClass('visible');
2107 $('#floating-chatbot-button').removeClass('hidden');
2108 enableScroll();
2109 });
2110
2111 $(document).on('click', '.close-pre-chat-message', function(e) {
2112 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2113 $('#pre-chat-message').fadeOut(200, function() {
2114 $(this).remove();
2115 });
2116 });
2117
2118
2119 // PDF upload button handlers
2120 if (document.getElementById('pdf-upload-btn')) {
2121 document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2122 document.getElementById('pdf-upload').click();
2123 });
2124 }
2125
2126 // Word upload button handlers
2127 if (document.getElementById('word-upload-btn')) {
2128 document.getElementById('word-upload-btn').addEventListener('click', function() {
2129 document.getElementById('word-upload').click();
2130 });
2131 }
2132
2133 // PDF file input change handler
2134 addSafeEventListener('pdf-upload', 'change', async function(e) {
2135 const file = e.target.files[0];
2136
2137 if (!file || file.type !== 'application/pdf') {
2138 alert('Please select a valid PDF file.');
2139 return;
2140 }
2141
2142 if (!sessionId) {
2143 //console.error('No session ID found');
2144 alert('Error: No session ID found');
2145 return;
2146 }
2147
2148 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2149 //console.error('mxchatChat not properly configured:', mxchatChat);
2150 alert('Error: Ajax configuration missing');
2151 return;
2152 }
2153
2154 // Disable buttons and show loading state
2155 const uploadBtn = document.getElementById('pdf-upload-btn');
2156 const sendBtn = document.getElementById('send-button');
2157 const originalBtnContent = uploadBtn.innerHTML;
2158
2159 try {
2160 const formData = new FormData();
2161 formData.append('action', 'mxchat_upload_pdf');
2162 formData.append('pdf_file', file);
2163 formData.append('session_id', sessionId);
2164 formData.append('nonce', mxchatChat.nonce);
2165
2166 uploadBtn.disabled = true;
2167 sendBtn.disabled = true;
2168 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2169 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2170 </svg>`;
2171
2172 const response = await fetch(mxchatChat.ajax_url, {
2173 method: 'POST',
2174 body: formData
2175 });
2176
2177 const data = await response.json();
2178
2179 if (data.success) {
2180 // Hide popular questions if they exist
2181 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2182 if (hasQuickQuestions()) {
2183 collapseQuickQuestions();
2184 }
2185
2186 // Show the active PDF name
2187 showActivePdf(data.data.filename);
2188
2189 appendMessage('bot', data.data.message);
2190 scrollToBottom();
2191 activePdfFile = data.data.filename;
2192 } else {
2193 //console.error('Upload failed:', data.data);
2194 alert('Failed to upload PDF. Please try again.');
2195 }
2196 } catch (error) {
2197 //console.error('Upload error:', error);
2198 alert('Error uploading file. Please try again.');
2199 } finally {
2200 uploadBtn.disabled = false;
2201 sendBtn.disabled = false;
2202 uploadBtn.innerHTML = originalBtnContent;
2203 this.value = ''; // Reset file input
2204 }
2205 });
2206
2207 // Word file input change handler
2208 addSafeEventListener('word-upload', 'change', async function(e) {
2209 const file = e.target.files[0];
2210
2211 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2212 alert('Please select a valid Word document (.docx).');
2213 return;
2214 }
2215
2216 if (!sessionId) {
2217 //console.error('No session ID found');
2218 alert('Error: No session ID found');
2219 return;
2220 }
2221
2222 // Disable buttons and show loading state
2223 const uploadBtn = document.getElementById('word-upload-btn');
2224 const sendBtn = document.getElementById('send-button');
2225 const originalBtnContent = uploadBtn.innerHTML;
2226
2227 try {
2228 const formData = new FormData();
2229 formData.append('action', 'mxchat_upload_word');
2230 formData.append('word_file', file);
2231 formData.append('session_id', sessionId);
2232 formData.append('nonce', mxchatChat.nonce);
2233
2234 uploadBtn.disabled = true;
2235 sendBtn.disabled = true;
2236 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2237 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2238 </svg>`;
2239
2240 const response = await fetch(mxchatChat.ajax_url, {
2241 method: 'POST',
2242 body: formData
2243 });
2244
2245 const data = await response.json();
2246
2247 if (data.success) {
2248 // Hide popular questions if they exist
2249 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2250 if (hasQuickQuestions()) {
2251 collapseQuickQuestions();
2252 }
2253
2254 // Show the active Word document name
2255 showActiveWord(data.data.filename);
2256
2257 appendMessage('bot', data.data.message);
2258 scrollToBottom();
2259 activeWordFile = data.data.filename;
2260 } else {
2261 //console.error('Upload failed:', data.data);
2262 alert('Failed to upload Word document. Please try again.');
2263 }
2264 } catch (error) {
2265 //console.error('Upload error:', error);
2266 alert('Error uploading file. Please try again.');
2267 } finally {
2268 uploadBtn.disabled = false;
2269 sendBtn.disabled = false;
2270 uploadBtn.innerHTML = originalBtnContent;
2271 this.value = ''; // Reset file input
2272 }
2273 });
2274
2275 // Remove button click handlers
2276 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2277 e.preventDefault();
2278 e.stopPropagation();
2279 removeActivePdf();
2280 });
2281
2282 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2283 e.preventDefault();
2284 e.stopPropagation();
2285 removeActiveWord();
2286 });
2287
2288 // Window resize handlers
2289 $(window).on('resize orientationchange', function() {
2290 setFullHeight();
2291 });
2292
2293
2294 // ====================================
2295 // TOOLBAR & STYLING SETUP
2296 // ====================================
2297
2298 // Apply toolbar settings
2299 if (mxchatChat.chat_toolbar_toggle === 'on') {
2300 $('.chat-toolbar').show();
2301 } else {
2302 $('.chat-toolbar').hide();
2303 }
2304
2305 // Apply toolbar icon colors
2306 const toolbarElements = [
2307 '#mxchat-chatbot .toolbar-btn svg',
2308 '#mxchat-chatbot .active-pdf-name',
2309 '#mxchat-chatbot .active-word-name',
2310 '#mxchat-chatbot .remove-pdf-btn svg',
2311 '#mxchat-chatbot .remove-word-btn svg',
2312 '#mxchat-chatbot .toolbar-perplexity svg'
2313 ];
2314
2315 toolbarElements.forEach(selector => {
2316 $(selector).css({
2317 'fill': toolbarIconColor,
2318 'stroke': toolbarIconColor,
2319 'color': toolbarIconColor
2320 });
2321 });
2322
2323
2324 // ====================================
2325 // EMAIL COLLECTION SETUP - FIXED VERSION
2326 // ====================================
2327 // Only run email collection setup if it's enabled
2328 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2329 //console.log('Email collection is enabled, setting up handlers...');
2330
2331 // Email collection form setup and handlers
2332 const emailForm = document.getElementById('email-collection-form');
2333 const emailBlocker = document.getElementById('email-blocker');
2334 const chatbotWrapper = document.getElementById('chat-container');
2335
2336 if (emailForm && emailBlocker && chatbotWrapper) {
2337
2338 // Add loading state management
2339 let isSubmitting = false;
2340
2341 // Optimized UI transition functions
2342 function showEmailForm() {
2343 emailBlocker.style.display = 'flex';
2344 chatbotWrapper.style.display = 'none';
2345 }
2346
2347 function showChatContainer() {
2348 // Show chat immediately without delay
2349 emailBlocker.style.display = 'none';
2350 chatbotWrapper.style.display = 'flex';
2351
2352 // Load chat history only after showing chat container
2353 if (typeof loadChatHistory === 'function') {
2354 loadChatHistory();
2355 }
2356 }
2357
2358 // Enhanced email validation
2359 function isValidEmail(email) {
2360 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2361 return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2362 }
2363
2364 // Enhanced name validation
2365 function isValidName(name) {
2366 return name && name.trim().length >= 2 && name.trim().length <= 100;
2367 }
2368
2369 // Show loading state with spinner
2370 function setSubmissionState(loading) {
2371 const submitButton = document.getElementById('email-submit-button');
2372 const emailInput = document.getElementById('user-email');
2373 const nameInput = document.getElementById('user-name');
2374
2375 if (loading) {
2376 isSubmitting = true;
2377 if (submitButton) submitButton.disabled = true;
2378 if (emailInput) emailInput.disabled = true;
2379 if (nameInput) nameInput.disabled = true;
2380
2381 // Store original content and add spinner
2382 if (submitButton && !submitButton.getAttribute('data-original-html')) {
2383 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2384
2385 // Add loading spinner while keeping original text
2386 const originalText = submitButton.textContent;
2387 submitButton.innerHTML = `
2388 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2389 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2390 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2391 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2392 </circle>
2393 </svg>
2394 ${originalText}
2395 `;
2396
2397 submitButton.style.opacity = '0.8';
2398 }
2399 } else {
2400 isSubmitting = false;
2401 if (submitButton) submitButton.disabled = false;
2402 if (emailInput) emailInput.disabled = false;
2403 if (nameInput) nameInput.disabled = false;
2404
2405 // Restore original content
2406 if (submitButton) {
2407 const originalHtml = submitButton.getAttribute('data-original-html');
2408 if (originalHtml) {
2409 submitButton.innerHTML = originalHtml;
2410 }
2411 submitButton.style.opacity = '1';
2412 }
2413 }
2414 }
2415
2416 // Error display functions
2417 function showEmailError(message) {
2418 clearEmailError();
2419
2420 const errorDiv = document.createElement('div');
2421 errorDiv.className = 'email-error';
2422 errorDiv.style.cssText = `
2423 color: #e74c3c;
2424 font-size: 12px;
2425 margin-top: 8px;
2426 padding: 4px 0;
2427 animation: fadeInError 0.3s ease;
2428 `;
2429 errorDiv.textContent = message;
2430
2431 // Add CSS animation if not already present
2432 if (!document.getElementById('email-error-styles')) {
2433 const style = document.createElement('style');
2434 style.id = 'email-error-styles';
2435 style.textContent = `
2436 @keyframes fadeInError {
2437 from { opacity: 0; transform: translateY(-5px); }
2438 to { opacity: 1; transform: translateY(0); }
2439 }
2440 .email-input-shake {
2441 animation: shake 0.5s ease-in-out;
2442 }
2443 @keyframes shake {
2444 0%, 100% { transform: translateX(0); }
2445 25% { transform: translateX(-5px); }
2446 75% { transform: translateX(5px); }
2447 }
2448 @keyframes spin {
2449 from { transform: rotate(0deg); }
2450 to { transform: rotate(360deg); }
2451 }
2452 .email-spinner {
2453 display: inline-block;
2454 vertical-align: middle;
2455 }
2456 `;
2457 document.head.appendChild(style);
2458 }
2459
2460 emailForm.appendChild(errorDiv);
2461
2462 // Add shake animation to inputs
2463 const emailInput = document.getElementById('user-email');
2464 const nameInput = document.getElementById('user-name');
2465
2466 if (emailInput) {
2467 emailInput.classList.add('email-input-shake');
2468 setTimeout(() => {
2469 emailInput.classList.remove('email-input-shake');
2470 }, 500);
2471 }
2472
2473 if (nameInput) {
2474 nameInput.classList.add('email-input-shake');
2475 setTimeout(() => {
2476 nameInput.classList.remove('email-input-shake');
2477 }, 500);
2478 }
2479 }
2480
2481 function clearEmailError() {
2482 const existingErrors = emailForm.querySelectorAll('.email-error');
2483 existingErrors.forEach(error => error.remove());
2484 }
2485
2486 // MAIN FORM SUBMIT HANDLER - This is the critical fix
2487 //console.log('Setting up form submit handler...');
2488
2489 // Remove any existing event listeners first
2490 emailForm.removeEventListener('submit', handleFormSubmit);
2491
2492 // Add the form submit handler
2493 emailForm.addEventListener('submit', handleFormSubmit);
2494
2495 function handleFormSubmit(event) {
2496 //console.log('Form submit handler triggered!');
2497 event.preventDefault();
2498 event.stopPropagation();
2499
2500 // Prevent double submission
2501 if (isSubmitting) {
2502 //console.log('Already submitting, ignoring...');
2503 return false;
2504 }
2505
2506 const userEmail = document.getElementById('user-email').value.trim();
2507 const nameInput = document.getElementById('user-name');
2508 const userName = nameInput ? nameInput.value.trim() : '';
2509 const sessionId = getChatSession();
2510
2511 //console.log('Form data:', { userEmail, userName, sessionId });
2512
2513 // Validate email before submission
2514 if (!userEmail) {
2515 showEmailError('Please enter your email address.');
2516 return false;
2517 }
2518
2519 if (!isValidEmail(userEmail)) {
2520 showEmailError('Please enter a valid email address.');
2521 return false;
2522 }
2523
2524 // Validate name if field exists
2525 if (nameInput && !isValidName(userName)) {
2526 showEmailError('Please enter a valid name (2-100 characters).');
2527 return false;
2528 }
2529
2530 // Clear any existing errors
2531 clearEmailError();
2532 setSubmissionState(true);
2533
2534 //console.log('Sending AJAX request...');
2535
2536 // Prepare form data with optional name
2537 const formData = new URLSearchParams({
2538 action: 'mxchat_handle_save_email_and_response',
2539 email: userEmail,
2540 session_id: sessionId,
2541 nonce: mxchatChat.nonce,
2542 });
2543
2544 // Add name to form data if provided
2545 if (userName) {
2546 formData.append('name', userName);
2547 }
2548
2549 fetch(mxchatChat.ajax_url, {
2550 method: 'POST',
2551 headers: {
2552 'Content-Type': 'application/x-www-form-urlencoded',
2553 },
2554 body: formData
2555 })
2556 .then((response) => {
2557 //console.log('Response received:', response);
2558 if (!response.ok) {
2559 throw new Error(`HTTP error! status: ${response.status}`);
2560 }
2561 return response.json();
2562 })
2563 .then((data) => {
2564 //console.log('Response data:', data);
2565 setSubmissionState(false);
2566
2567 if (data.success) {
2568 // Show chat immediately
2569 showChatContainer();
2570
2571 // Handle bot response if provided
2572 if (data.message && typeof appendMessage === 'function') {
2573 setTimeout(() => {
2574 appendMessage('bot', data.message);
2575 if (typeof scrollToBottom === 'function') {
2576 scrollToBottom();
2577 }
2578 }, 100);
2579 }
2580 } else {
2581 showEmailError(data.message || 'Failed to save email. Please try again.');
2582 }
2583 })
2584 .catch((error) => {
2585 console.error('Email submission error:', error);
2586 setSubmissionState(false);
2587 showEmailError('An error occurred. Please try again.');
2588 });
2589
2590 return false; // Extra prevention
2591 }
2592
2593 // Real-time email validation
2594 const emailInput = document.getElementById('user-email');
2595 if (emailInput) {
2596 let validationTimeout;
2597
2598 emailInput.addEventListener('input', function() {
2599 // Clear previous validation timeout
2600 if (validationTimeout) {
2601 clearTimeout(validationTimeout);
2602 }
2603
2604 // Debounce validation
2605 validationTimeout = setTimeout(() => {
2606 const email = this.value.trim();
2607 clearEmailError();
2608
2609 if (email && !isValidEmail(email)) {
2610 showEmailError('Please enter a valid email address.');
2611 }
2612 }, 500);
2613 });
2614
2615 // Handle Enter key
2616 emailInput.addEventListener('keypress', function(e) {
2617 if (e.key === 'Enter' && !isSubmitting) {
2618 e.preventDefault();
2619 emailForm.dispatchEvent(new Event('submit'));
2620 }
2621 });
2622 }
2623
2624 // Real-time name validation
2625 const nameInput = document.getElementById('user-name');
2626 if (nameInput) {
2627 let nameValidationTimeout;
2628
2629 nameInput.addEventListener('input', function() {
2630 // Clear previous validation timeout
2631 if (nameValidationTimeout) {
2632 clearTimeout(nameValidationTimeout);
2633 }
2634
2635 // Debounce validation
2636 nameValidationTimeout = setTimeout(() => {
2637 const name = this.value.trim();
2638 clearEmailError();
2639
2640 if (name && !isValidName(name)) {
2641 showEmailError('Name must be between 2 and 100 characters.');
2642 }
2643 }, 500);
2644 });
2645
2646 // Handle Enter key
2647 nameInput.addEventListener('keypress', function(e) {
2648 if (e.key === 'Enter' && !isSubmitting) {
2649 e.preventDefault();
2650 emailForm.dispatchEvent(new Event('submit'));
2651 }
2652 });
2653 }
2654
2655 // Initial state check
2656 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2657 //console.log('Using server-provided email state:', mxchatChat.initial_email_state);
2658
2659 const emailState = mxchatChat.initial_email_state;
2660 if (emailState.show_email_form) {
2661 showEmailForm();
2662 } else {
2663 showChatContainer();
2664 }
2665 } else {
2666 // Check email status via AJAX
2667 setTimeout(checkSessionAndEmail, 100);
2668 }
2669
2670 // Check if email exists for the current session
2671 function checkSessionAndEmail() {
2672 const sessionId = getChatSession();
2673
2674 fetch(mxchatChat.ajax_url, {
2675 method: 'POST',
2676 headers: {
2677 'Content-Type': 'application/x-www-form-urlencoded',
2678 },
2679 body: new URLSearchParams({
2680 action: 'mxchat_check_email_provided',
2681 session_id: sessionId,
2682 nonce: mxchatChat.nonce,
2683 })
2684 })
2685 .then((response) => {
2686 if (!response.ok) {
2687 throw new Error(`HTTP error! status: ${response.status}`);
2688 }
2689 return response.json();
2690 })
2691 .then((data) => {
2692 if (data.success) {
2693 if (data.data.logged_in || data.data.email) {
2694 showChatContainer();
2695 } else {
2696 showEmailForm();
2697 }
2698 } else {
2699 // On error, default to showing email form
2700 showEmailForm();
2701 }
2702 })
2703 .catch((error) => {
2704 console.warn('Email check failed, defaulting to email form:', error);
2705 showEmailForm();
2706 });
2707 }
2708
2709 } else {
2710 console.error('Email collection is enabled but essential elements are missing:', {
2711 emailForm: !!emailForm,
2712 emailBlocker: !!emailBlocker,
2713 chatbotWrapper: !!chatbotWrapper
2714 });
2715 }
2716 } else {
2717 //console.log('Email collection is disabled');
2718 }
2719
2720 // Open chatbot when pre-chat message is clicked
2721 $(document).on('click', '#pre-chat-message', function() {
2722 var chatbot = $('#floating-chatbot');
2723 if (chatbot.hasClass('hidden')) {
2724 chatbot.removeClass('hidden').addClass('visible');
2725 $('#floating-chatbot-button').addClass('hidden');
2726 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
2727 disableScroll(); // Disable scroll when chatbot opens
2728 }
2729 });
2730
2731 var closeButton = document.querySelector('.close-pre-chat-message');
2732 if (closeButton) {
2733 closeButton.addEventListener('click', function() {
2734 $('#pre-chat-message').fadeOut(200); // Hide the message
2735
2736 // Send an AJAX request to set the transient flag for 24 hours
2737 $.ajax({
2738 url: mxchatChat.ajax_url,
2739 type: 'POST',
2740 data: {
2741 action: 'mxchat_dismiss_pre_chat_message',
2742 _ajax_nonce: mxchatChat.nonce
2743 },
2744 success: function() {
2745 //console.log('Pre-chat message dismissed for 24 hours.');
2746
2747 // Ensure the message is hidden after dismissal
2748 $('#pre-chat-message').hide();
2749 },
2750 error: function() {
2751 ////console.error('Failed to dismiss pre-chat message.');
2752 }
2753 });
2754 });
2755 }
2756
2757
2758 function hasQuickQuestions() {
2759 const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
2760 return questionButtons.length > 0;
2761 }
2762
2763 function collapseQuickQuestions() {
2764 const questionsContainer = document.getElementById('mxchat-popular-questions');
2765 if (questionsContainer && hasQuickQuestions()) {
2766 questionsContainer.classList.add('collapsed');
2767 questionsContainer.classList.add('has-been-collapsed');
2768 try {
2769 sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2770 sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
2771 } catch (e) {
2772 // Ignore if sessionStorage is not available
2773 }
2774 }
2775 }
2776
2777 function expandQuickQuestions() {
2778 const questionsContainer = document.getElementById('mxchat-popular-questions');
2779 if (questionsContainer && hasQuickQuestions()) {
2780 questionsContainer.classList.remove('collapsed');
2781 try {
2782 sessionStorage.setItem('mxchat_questions_collapsed', 'false');
2783 } catch (e) {
2784 // Ignore if sessionStorage is not available
2785 }
2786 }
2787 }
2788
2789 function checkQuickQuestionsState() {
2790 if (!hasQuickQuestions()) {
2791 return; // Don't do anything if no questions exist
2792 }
2793
2794 try {
2795 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2796 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2797
2798 const questionsContainer = document.getElementById('mxchat-popular-questions');
2799 if (questionsContainer) {
2800 if (hasBeenCollapsed === 'true') {
2801 questionsContainer.classList.add('has-been-collapsed');
2802 }
2803 if (isCollapsed === 'true') {
2804 questionsContainer.classList.add('collapsed');
2805 }
2806 }
2807 } catch (e) {
2808 // Ignore if sessionStorage is not available
2809 }
2810 }
2811
2812 // Global delegation for dynamically added links as fallback
2813 $(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
2814 const $link = $(this);
2815 const messageDiv = $link.closest('.bot-message, .agent-message');
2816
2817 // Only process bot/agent message links
2818 if (messageDiv.length > 0) {
2819 const originalHref = $link.attr('href');
2820
2821 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2822 e.preventDefault();
2823 e.stopPropagation();
2824
2825 // Mark as tracked
2826 $link.attr('data-tracked', 'true');
2827
2828 // Get message context from the message div
2829 const messageText = messageDiv.text().substring(0, 200);
2830
2831 $.ajax({
2832 url: mxchatChat.ajax_url,
2833 type: 'POST',
2834 data: {
2835 action: 'mxchat_track_url_click',
2836 session_id: getChatSession(),
2837 url: originalHref,
2838 message_context: messageText,
2839 nonce: mxchatChat.nonce
2840 },
2841 complete: function() {
2842 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2843 window.open(originalHref, '_blank');
2844 } else {
2845 window.location.href = originalHref;
2846 }
2847 }
2848 });
2849
2850 return false;
2851 }
2852 }
2853 });
2854
2855
2856
2857
2858 // ====================================
2859 // MAIN INITIALIZATION
2860 // ====================================
2861
2862 if ($('#floating-chatbot').hasClass('hidden')) {
2863 $('#floating-chatbot-button').removeClass('hidden');
2864 }
2865 // Initialize when document is ready
2866 setFullHeight();
2867 initializeChatVisibility();
2868 loadChatHistory();
2869 trackOriginatingPage();
2870
2871 // Make functions globally available for add-ons
2872 window.hasQuickQuestions = hasQuickQuestions;
2873 window.collapseQuickQuestions = collapseQuickQuestions;
2874 window.appendMessage = appendMessage;
2875 window.appendThinkingMessage = appendThinkingMessage;
2876 window.scrollToBottom = scrollToBottom;
2877 window.scrollElementToTop = scrollElementToTop;
2878 window.replaceLastMessage = replaceLastMessage;
2879 window.callMxChat = callMxChat;
2880 window.callMxChatStream = callMxChatStream;
2881 window.shouldUseStreaming = shouldUseStreaming;
2882 window.getChatSession = getChatSession;
2883 window.getPageContext = getPageContext;
2884 window.updateStreamingMessage = updateStreamingMessage;
2885
2886 }); // End of jQuery ready
2887
2888
2889 // ====================================
2890 // GLOBAL EVENT LISTENERS (Outside jQuery)
2891 // ====================================
2892
2893 // Event listener for copy button (code blocks)
2894 document.addEventListener("click", (e) => {
2895 if (e.target.classList.contains("mxchat-copy-button")) {
2896 const copyButton = e.target;
2897 const codeBlock = copyButton
2898 .closest(".mxchat-code-block-container")
2899 .querySelector(".mxchat-code-block code");
2900
2901 if (codeBlock) {
2902 // Preserve formatting using innerText
2903 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
2904 copyButton.textContent = "Copied!";
2905 copyButton.setAttribute("aria-label", "Copied to clipboard");
2906
2907 setTimeout(() => {
2908 copyButton.textContent = "Copy";
2909 copyButton.setAttribute("aria-label", "Copy to clipboard");
2910 }, 2000);
2911 });
2912 }
2913 }
2914 });
2915
2916