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

2,029 lines 75.9 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 // Get text content and clean it up
138 pageContent = clone.textContent || clone.innerText || '';
139
140 // Clean up whitespace and limit length
141 pageContent = pageContent
142 .replace(/\s+/g, ' ')
143 .trim()
144 .substring(0, 3000); // Limit to 3000 characters to avoid token limits
145 }
146
147 // Only return context if we have meaningful content
148 if (!pageContent || pageContent.length < 50) {
149 return null;
150 }
151
152 return {
153 url: pageUrl,
154 title: pageTitle,
155 content: pageContent
156 };
157 }
158
159 // ====================================
160 // CORE CHAT FUNCTIONALITY
161 // ====================================
162 // Update your existing sendMessage function
163 function sendMessage() {
164 var message = $('#chat-input').val();
165 if (message) {
166 appendMessage("user", message);
167 $('#chat-input').val('');
168 $('#chat-input').css('height', 'auto');
169
170 if (hasQuickQuestions()) {
171 collapseQuickQuestions();
172 }
173 appendThinkingMessage();
174 scrollToBottom();
175
176 const currentModel = mxchatChat.model || 'gpt-4o';
177
178 // Check if streaming is enabled AND supported for this model
179 if (shouldUseStreaming(currentModel)) {
180 callMxChatStream(message, function(response) {
181 $('.bot-message.temporary-message').removeClass('temporary-message');
182 });
183 } else {
184 callMxChat(message, function(response) {
185 replaceLastMessage("bot", response);
186 });
187 }
188 }
189 }
190
191 // Update your existing sendMessageToChatbot function
192 function sendMessageToChatbot(message) {
193 var sessionId = getChatSession();
194
195 if (hasQuickQuestions()) {
196 collapseQuickQuestions();
197 }
198 appendThinkingMessage();
199 scrollToBottom();
200
201 const currentModel = mxchatChat.model || 'gpt-4o';
202
203 // Check if streaming is enabled AND supported for this model
204 if (shouldUseStreaming(currentModel)) {
205 callMxChatStream(message, function(response) {
206 $('.bot-message.temporary-message').removeClass('temporary-message');
207 });
208 } else {
209 callMxChat(message, function(response) {
210 $('.temporary-message').remove();
211 replaceLastMessage("bot", response);
212 });
213 }
214 }
215
216
217 // Updated shouldUseStreaming function with debugging
218 function shouldUseStreaming(model) {
219 // Check if streaming is enabled in settings (using your toggle naming pattern)
220 const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
221
222 // Check if model supports streaming
223 const streamingSupported = isStreamingSupported(model);
224
225
226 // Only use streaming if both enabled and supported
227 return streamingEnabled && streamingSupported;
228 }
229
230 function callMxChat(message, callback) {
231 // Get page context if contextual awareness is enabled
232 const pageContext = getPageContext();
233
234 // Prepare AJAX data
235 const ajaxData = {
236 action: 'mxchat_handle_chat_request',
237 message: message,
238 session_id: getChatSession(),
239 nonce: mxchatChat.nonce
240 };
241
242 // Add page context if available
243 if (pageContext) {
244 ajaxData.page_context = JSON.stringify(pageContext);
245 }
246
247 $.ajax({
248 url: mxchatChat.ajax_url,
249 type: 'POST',
250 dataType: 'json',
251 data: ajaxData,
252 success: function(response) {
253 // Log the full response for debugging
254 //console.log("API Response:", response);
255
256 // First check if this is a successful response by looking for text, html, or message fields
257 // This preserves compatibility with your server response format
258 if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
259 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
260
261 // Handle successful response - this is your original success handling code
262
263 // Existing chat mode check
264 if (response.chat_mode) {
265 updateChatModeIndicator(response.chat_mode);
266 }
267 else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
268 updateChatModeIndicator(response.fallbackResponse.chat_mode);
269 }
270
271 // Add PDF filename handling
272 if (response.data && response.data.filename) {
273 showActivePdf(response.data.filename);
274 activePdfFile = response.data.filename;
275 }
276
277 // Add redirect check here
278 if (response.redirect_url) {
279 let responseText = response.text || '';
280 if (responseText) {
281 replaceLastMessage("bot", responseText);
282 }
283 setTimeout(() => {
284 window.location.href = response.redirect_url;
285 }, 1500);
286 return;
287 }
288
289 // Check for live agent response
290 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
291 updateChatModeIndicator('agent');
292 return;
293 }
294
295 // Handle other responses
296 let responseText = response.text || '';
297 let responseHtml = response.html || '';
298 let responseMessage = response.message || '';
299
300 if (responseText === 'You are now chatting with the AI chatbot.') {
301 updateChatModeIndicator('ai');
302 }
303
304 // Handle the message and show notification if chat is hidden
305 if (responseText || responseHtml || responseMessage) {
306 // Update the messages as before
307 if (responseText && responseHtml) {
308 replaceLastMessage("bot", responseText, responseHtml);
309 } else if (responseText) {
310 replaceLastMessage("bot", responseText);
311 } else if (responseHtml) {
312 replaceLastMessage("bot", "", responseHtml);
313 } else if (responseMessage) {
314 replaceLastMessage("bot", responseMessage);
315 }
316
317 // Check if chat is hidden and show notification
318 if ($('#floating-chatbot').hasClass('hidden')) {
319 const badge = $('#chat-notification-badge');
320 if (badge.length) {
321 badge.show();
322 }
323 }
324 } else {
325 ////console.error("Unexpected response format:", response);
326 replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
327 }
328
329 if (response.message_id) {
330 lastSeenMessageId = response.message_id;
331 }
332
333 return;
334 }
335
336 // If we got here, it's likely an error response
337 // Now we can check for error conditions with our robust error handling
338
339 let errorMessage = "";
340 let errorCode = "";
341
342 // Check various possible error locations in the response
343 if (response.data && response.data.error_message) {
344 errorMessage = response.data.error_message;
345 errorCode = response.data.error_code || "";
346 } else if (response.error_message) {
347 errorMessage = response.error_message;
348 errorCode = response.error_code || "";
349 } else if (response.message) {
350 errorMessage = response.message;
351 } else if (typeof response.data === 'string') {
352 errorMessage = response.data;
353 } else if (!response.success) {
354 // Explicit check for success: false without other error info
355 errorMessage = "An error occurred. Please try again or contact support.";
356 } else {
357 // Fallback for any other unexpected response format
358 errorMessage = "Unexpected response received. Please try again or contact support.";
359 }
360
361 // Log the error with code for debugging
362 //console.log("Response data:", response.data);
363 ////console.error("API Error:", errorMessage, "Code:", errorCode);
364
365 // Format user-friendly error message
366 let displayMessage = errorMessage;
367
368 // Customize message for admin users
369 if (mxchatChat.is_admin) {
370 // For admin users, show more technical details including error code
371 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
372 }
373
374 replaceLastMessage("bot", displayMessage);
375 },
376 error: function(xhr, status, error) {
377 //console.error("AJAX Error:", status, error);
378 //console.log("Response Text:", xhr.responseText);
379
380 let errorMessage = "An unexpected error occurred.";
381
382 // Try to parse the response if it's JSON
383 try {
384 const responseJson = JSON.parse(xhr.responseText);
385 //console.log("Parsed error response:", responseJson);
386
387 if (responseJson.data && responseJson.data.error_message) {
388 errorMessage = responseJson.data.error_message;
389 } else if (responseJson.message) {
390 errorMessage = responseJson.message;
391 }
392 } catch (e) {
393 // Not JSON or parsing failed, use HTTP status based messages
394 if (xhr.status === 0) {
395 errorMessage = "Network error: Please check your internet connection.";
396 } else if (xhr.status === 403) {
397 errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
398 } else if (xhr.status === 404) {
399 errorMessage = "API endpoint not found. Please contact support.";
400 } else if (xhr.status === 429) {
401 errorMessage = "Too many requests. Please try again in a moment.";
402 } else if (xhr.status >= 500) {
403 errorMessage = "Server error: The server encountered an issue. Please try again later.";
404 }
405 }
406
407 replaceLastMessage("bot", errorMessage);
408 }
409 });
410 }
411
412 function callMxChatStream(message, callback) {
413 //console.log("Using streaming for message:", message);
414
415 const currentModel = mxchatChat.model || 'gpt-4o';
416 if (!isStreamingSupported(currentModel)) {
417 //console.log("Streaming not supported, falling back to regular call");
418 callMxChat(message, callback);
419 return;
420 }
421
422 // Get page context if contextual awareness is enabled
423 const pageContext = getPageContext();
424
425 const formData = new FormData();
426 formData.append('action', 'mxchat_stream_chat');
427 formData.append('message', message);
428 formData.append('session_id', getChatSession());
429 formData.append('nonce', mxchatChat.nonce);
430
431 // Add page context if available
432 if (pageContext) {
433 formData.append('page_context', JSON.stringify(pageContext));
434 }
435
436 let accumulatedContent = '';
437 let testingDataReceived = false;
438
439 fetch(mxchatChat.ajax_url, {
440 method: 'POST',
441 body: formData,
442 credentials: 'same-origin'
443 })
444 .then(response => {
445 //console.log("Streaming response received:", response);
446
447 if (!response.ok) {
448 throw new Error('Network response was not ok');
449 }
450
451 // Check if response is JSON instead of streaming
452 const contentType = response.headers.get('content-type');
453 if (contentType && contentType.includes('application/json')) {
454 //console.log("Received JSON response instead of stream, handling as regular response");
455 return response.json().then(data => {
456
457 // FIXED: Always check for testing panel, not just in testing mode
458 if (window.mxchatTestPanelInstance && data.testing_data) {
459 //console.log('Testing data found in streaming JSON response:', data.testing_data);
460 window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
461 }
462
463 // Handle as regular JSON response
464 $('.bot-message.temporary-message').remove();
465
466 // Handle different response formats (including intent responses)
467 if (data.text || data.html || data.message) {
468 if (data.text && data.html) {
469 replaceLastMessage("bot", data.text, data.html);
470 } else if (data.text) {
471 replaceLastMessage("bot", data.text);
472 } else if (data.html) {
473 replaceLastMessage("bot", "", data.html);
474 } else if (data.message) {
475 replaceLastMessage("bot", data.message);
476 }
477 }
478
479 // Handle other response properties
480 if (data.chat_mode) {
481 updateChatModeIndicator(data.chat_mode);
482 }
483
484 if (data.data && data.data.filename) {
485 showActivePdf(data.data.filename);
486 activePdfFile = data.data.filename;
487 }
488
489 if (callback) {
490 callback(data.text || data.message || '');
491 }
492 });
493 }
494
495 // Continue with streaming processing
496 const reader = response.body.getReader();
497 const decoder = new TextDecoder();
498 let buffer = '';
499
500 function processStream() {
501 reader.read().then(({ done, value }) => {
502 if (done) {
503 //console.log("Streaming completed, final content:", accumulatedContent);
504 if (callback) {
505 callback(accumulatedContent);
506 }
507 return;
508 }
509
510 buffer += decoder.decode(value, { stream: true });
511 const lines = buffer.split('\n');
512 buffer = lines.pop() || '';
513
514 for (const line of lines) {
515 if (line.startsWith('data: ')) {
516 const data = line.substring(6);
517
518 if (data === '[DONE]') {
519 //console.log("Received [DONE] signal");
520 if (callback) {
521 callback(accumulatedContent);
522 }
523 return;
524 }
525
526 try {
527 const json = JSON.parse(data);
528
529 // FIXED: Always check for testing panel and handle testing data properly
530 if (json.testing_data && !testingDataReceived) {
531 //console.log('Testing data received in stream:', json.testing_data);
532 if (window.mxchatTestPanelInstance) {
533 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
534 testingDataReceived = true;
535 }
536 }
537 // Handle content streaming
538 else if (json.content) {
539 accumulatedContent += json.content;
540 updateStreamingMessage(accumulatedContent);
541 }
542 // Handle errors
543 else if (json.error) {
544 //console.error("Streaming error:", json.error);
545 replaceLastMessage("bot", "Error: " + json.error);
546 return;
547 }
548 } catch (e) {
549 //console.error('Error parsing SSE data:', e, 'Data:', data);
550 }
551 }
552 }
553
554 processStream();
555 });
556 }
557
558 processStream();
559 })
560 .catch(error => {
561 //console.error('Streaming error:', error);
562 callMxChat(message, callback);
563 });
564 }
565
566
567 // Function to update message during streaming
568 function updateStreamingMessage(content) {
569 const formattedContent = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(content))));
570
571 // Find the temporary message
572 const tempMessage = $('.bot-message.temporary-message').last();
573
574 if (tempMessage.length) {
575 // Update existing message
576 tempMessage.html(formattedContent);
577 } else {
578 // Create new temporary message if it doesn't exist
579 appendMessage("bot", content, '', [], true);
580 }
581
582 }
583
584 // UPGRADE: Function to check if streaming is supported for the current model
585 function isStreamingSupported(model) {
586 if (!model) return false;
587
588 //console.log("Checking streaming support for model:", model); // Debug log
589
590 // Get the model prefix
591 const modelPrefix = model.split('-')[0].toLowerCase();
592
593 //console.log("Model prefix:", modelPrefix); // Debug log
594
595 // Support streaming for OpenAI, Claude, and Grok models
596 const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude' || modelPrefix === 'grok';
597
598 //console.log("Streaming supported:", isSupported); // Debug log
599
600 return isSupported;
601 }
602
603 // Update the event handlers to use the correct function names
604 $('#send-button').off('click').on('click', function() {
605 sendMessage(); // Use the updated sendMessage function
606 });
607
608 // Override enter key handler
609 $('#chat-input').off('keypress').on('keypress', function(e) {
610 if (e.which == 13 && !e.shiftKey) {
611 e.preventDefault();
612 sendMessage(); // Use the updated sendMessage function
613 }
614 });
615
616
617 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
618 try {
619 // Determine styles based on sender type
620 let messageClass, bgColor, fontColor;
621
622 if (sender === "user") {
623 messageClass = "user-message";
624 bgColor = userMessageBgColor;
625 fontColor = userMessageFontColor;
626 // Only sanitize user input
627 messageText = sanitizeUserInput(messageText);
628 } else if (sender === "agent") {
629 messageClass = "agent-message";
630 bgColor = liveAgentMessageBgColor;
631 fontColor = liveAgentMessageFontColor;
632 } else {
633 messageClass = "bot-message";
634 bgColor = botMessageBgColor;
635 fontColor = botMessageFontColor;
636 }
637
638 const messageDiv = $('<div>')
639 .addClass(messageClass)
640 .attr('dir', 'auto') // Add dir="auto" for automatic text direction
641 .css({
642 'background': bgColor,
643 'color': fontColor,
644 'margin-bottom': '1em'
645 });
646
647 // Process the message content based on sender
648 let fullMessage;
649 if (sender === "user") {
650 // For user messages, apply linkify after sanitization
651 fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
652 } else {
653 // For bot/agent messages, preserve HTML
654 fullMessage = messageText;
655 }
656
657 // Add images if provided
658 if (images && images.length > 0) {
659 fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
660 images.forEach(img => {
661 // Ensure image URLs and titles are properly escaped
662 const safeTitle = sanitizeUserInput(img.title);
663 const safeUrl = encodeURI(img.image_url);
664 const safeThumbnail = encodeURI(img.thumbnail_url);
665
666 fullMessage += `
667 <div style="margin-bottom: 10px;">
668 <strong>${safeTitle}</strong><br>
669 <a href="${safeUrl}" target="_blank">
670 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
671 </a>
672 </div>`;
673 });
674 fullMessage += '</div>';
675 }
676
677 // Append HTML content if provided
678 if (messageHtml && sender !== "user") {
679 fullMessage += '<br><br>' + messageHtml;
680 }
681
682 messageDiv.html(fullMessage);
683
684 if (isTemporary) {
685 messageDiv.addClass('temporary-message');
686 }
687
688 messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
689 if (sender === "bot") {
690 const lastUserMessage = $('#chat-box').find('.user-message').last();
691 if (lastUserMessage.length) {
692 scrollElementToTop(lastUserMessage);
693 }
694 }
695 });
696
697 if (messageText.id) {
698 lastSeenMessageId = messageText.id;
699 hideNotification();
700 }
701 } catch (error) {
702 //console.error("Error rendering message:", error);
703 }
704 }
705
706 function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
707 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
708 var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
709
710 // Determine styles
711 let bgColor, fontColor;
712 if (sender === "user") {
713 bgColor = userMessageBgColor;
714 fontColor = userMessageFontColor;
715 } else if (sender === "agent") {
716 bgColor = liveAgentMessageBgColor;
717 fontColor = liveAgentMessageFontColor;
718 } else {
719 bgColor = botMessageBgColor;
720 fontColor = botMessageFontColor;
721 }
722
723 var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
724 if (responseHtml) {
725 fullMessage += '<br><br>' + responseHtml;
726 }
727
728 if (images.length > 0) {
729 fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
730 images.forEach(img => {
731 fullMessage += `
732 <div style="margin-bottom: 10px;">
733 <strong>${img.title}</strong><br>
734 <a href="${img.image_url}" target="_blank">
735 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
736 </a>
737 </div>`;
738 });
739 fullMessage += '</div>';
740 }
741
742 if (lastMessageDiv.length) {
743 lastMessageDiv.fadeOut(200, function() {
744 $(this)
745 .html(fullMessage)
746 .removeClass('bot-message user-message')
747 .addClass(messageClass)
748 .attr('dir', 'auto') // Add dir="auto" for automatic text direction
749 .css({
750 'background-color': bgColor,
751 'color': fontColor,
752 })
753 .removeClass('temporary-message')
754 .fadeIn(200, function() {
755 if (sender === "bot" || sender === "agent") {
756 const lastUserMessage = $('#chat-box').find('.user-message').last();
757 if (lastUserMessage.length) {
758 scrollElementToTop(lastUserMessage);
759 }
760 // Show notification if chat is hidden
761 if ($('#floating-chatbot').hasClass('hidden')) {
762 showNotification();
763 }
764 }
765 });
766 });
767 } else {
768 appendMessage(sender, responseText, responseHtml, images);
769 }
770 }
771
772 function appendThinkingMessage() {
773 // Remove any existing thinking dots first
774 $('.thinking-dots').remove();
775
776 // Retrieve the bot message font color and background color
777 var botMessageFontColor = mxchatChat.bot_message_font_color;
778 var botMessageBgColor = mxchatChat.bot_message_bg_color;
779
780
781 var thinkingHtml = '<div class="thinking-dots-container">' +
782 '<div class="thinking-dots">' +
783 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
784 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
785 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
786 '</div>' +
787 '</div>';
788
789 // Append the thinking dots to the chat container (or within the temporary message div)
790 $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
791 scrollToBottom();
792 }
793
794 function removeThinkingDots() {
795 $('.thinking-dots').closest('.temporary-message').remove();
796 }
797
798
799 // ====================================
800 // TEXT FORMATTING & PROCESSING
801 // ====================================
802
803 function linkify(inputText) {
804 if (!inputText) return '';
805
806 // Process markdown headers
807 let processedText = formatMarkdownHeaders(inputText);
808
809 // Process markdown links
810 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
811 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
812 const safeUrl = encodeURI(url);
813 const safeText = sanitizeUserInput(text);
814 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
815 });
816
817 // Process phone numbers (tel:)
818 const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
819 processedText = processedText.replace(phonePattern, (match, text, phone) => {
820 const safePhone = encodeURI(phone);
821 const safeText = sanitizeUserInput(text);
822 return `<a href="${safePhone}">${safeText}</a>`;
823 });
824
825 // Process standalone URLs
826 const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
827 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
828 const safeUrl = encodeURI(url);
829 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
830 });
831
832 // Process www. URLs
833 const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
834 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
835 const safeUrl = encodeURI(`http://${url}`);
836 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
837 });
838
839 return processedText;
840 }
841
842 function formatMarkdownHeaders(text) {
843 // Handle h1 to h6 headers
844 return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
845 const level = hashes.length;
846 return `<h${level} class="chat-heading">${content}</h${level}>`;
847 });
848 }
849
850 function formatBoldText(text) {
851 return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
852 }
853
854 function convertNewlinesToBreaks(text) {
855 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
856 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
857
858 // Wrap each paragraph in <p> tags
859 return paragraphs
860 .map(para => `<p>${para.trim()}</p>`)
861 .join('');
862 }
863
864 function formatCodeBlocks(text) {
865 // First handle raw PHP tags
866 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
867 return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
868 });
869
870 // Then handle code blocks with backticks
871 text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
872 return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
873 });
874
875 return text;
876 }
877
878 function sanitizeUserInput(text) {
879 const div = document.createElement('div');
880 div.textContent = text;
881 return div.innerHTML;
882 }
883
884
885 function escapeHtml(unsafe) {
886 // First check if it's already a code block
887 if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
888 return unsafe;
889 }
890
891 return unsafe
892 .replace(/&/g, "&amp;")
893 .replace(/</g, "&lt;")
894 .replace(/>/g, "&gt;")
895 .replace(/"/g, "&quot;")
896 .replace(/'/g, "&#039;");
897 }
898
899 function decodeHTMLEntities(text) {
900 var textArea = document.createElement('textarea');
901 textArea.innerHTML = text;
902 return textArea.value;
903 }
904
905
906 // ====================================
907 // UI & SCROLLING CONTROLS
908 // ====================================
909
910 function scrollToBottom(instant = false) {
911 var chatBox = $('#chat-box');
912 if (instant) {
913 // Instantly set the scroll position to the bottom
914 chatBox.scrollTop(chatBox.prop("scrollHeight"));
915 } else {
916 // Use requestAnimationFrame for smoother scrolling if needed
917 let start = null;
918 const scrollHeight = chatBox.prop("scrollHeight");
919 const initialScroll = chatBox.scrollTop();
920 const distance = scrollHeight - initialScroll;
921 const duration = 500; // Duration in ms
922
923 function smoothScroll(timestamp) {
924 if (!start) start = timestamp;
925 const progress = timestamp - start;
926 const currentScroll = initialScroll + (distance * (progress / duration));
927 chatBox.scrollTop(currentScroll);
928
929 if (progress < duration) {
930 requestAnimationFrame(smoothScroll);
931 } else {
932 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
933 }
934 }
935
936 requestAnimationFrame(smoothScroll);
937 }
938 }
939
940 function scrollElementToTop(element) {
941 var chatBox = $('#chat-box');
942 var elementTop = element.position().top + chatBox.scrollTop();
943 chatBox.animate({ scrollTop: elementTop }, 500);
944 }
945
946 function showChatWidget() {
947 // First ensure display is set
948 $('#floating-chatbot-button').css('display', 'flex');
949 // Then handle the fade
950 $('#floating-chatbot-button').fadeTo(500, 1);
951 // Force visibility
952 $('#floating-chatbot-button').removeClass('hidden');
953 //console.log('Showing widget');
954 }
955
956 function hideChatWidget() {
957 $('#floating-chatbot-button').css('display', 'none');
958 $('#floating-chatbot-button').addClass('hidden');
959 //console.log('Hiding widget');
960 }
961
962 function disableScroll() {
963 if (isMobile()) {
964 $('body').css('overflow', 'hidden');
965 }
966 }
967
968 function enableScroll() {
969 if (isMobile()) {
970 $('body').css('overflow', '');
971 }
972 }
973
974 function isMobile() {
975 // This can be a simple check, or more sophisticated detection of mobile devices
976 return window.innerWidth <= 768; // Example threshold for mobile devices
977 }
978
979 function setFullHeight() {
980 var vh = $(window).innerHeight() * 0.01;
981 $(':root').css('--vh', vh + 'px');
982 }
983
984
985 // ====================================
986 // NOTIFICATION SYSTEM
987 // ====================================
988
989 function createNotificationBadge() {
990 //console.log("Creating notification badge...");
991 const chatButton = document.getElementById('floating-chatbot-button');
992 //console.log("Chat button found:", !!chatButton);
993
994 if (!chatButton) return;
995
996 // Remove any existing badge first
997 const existingBadge = chatButton.querySelector('.chat-notification-badge');
998 if (existingBadge) {
999 //console.log("Removing existing badge");
1000 existingBadge.remove();
1001 }
1002
1003 notificationBadge = document.createElement('div');
1004 notificationBadge.className = 'chat-notification-badge';
1005 notificationBadge.style.cssText = `
1006 display: none;
1007 position: absolute;
1008 top: -5px;
1009 right: -5px;
1010 background-color: red;
1011 color: white;
1012 border-radius: 50%;
1013 padding: 4px 8px;
1014 font-size: 12px;
1015 font-weight: bold;
1016 z-index: 10001;
1017 `;
1018 chatButton.style.position = 'relative';
1019 chatButton.appendChild(notificationBadge);
1020
1021 }
1022
1023 function showNotification() {
1024 const badge = document.getElementById('chat-notification-badge');
1025 if (badge && $('#floating-chatbot').hasClass('hidden')) {
1026 badge.style.display = 'block';
1027 badge.textContent = '1';
1028 }
1029 }
1030
1031 function hideNotification() {
1032 const badge = document.getElementById('chat-notification-badge');
1033 if (badge) {
1034 badge.style.display = 'none';
1035 }
1036 }
1037
1038 function startNotificationChecking() {
1039 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1040 if (!chatPersistenceEnabled) return;
1041
1042 createNotificationBadge();
1043 notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
1044 }
1045
1046 function stopNotificationChecking() {
1047 if (notificationCheckInterval) {
1048 clearInterval(notificationCheckInterval);
1049 }
1050 }
1051
1052 function checkForNewMessages() {
1053 const sessionId = getChatSession();
1054 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1055
1056 if (!chatPersistenceEnabled) return;
1057
1058 $.ajax({
1059 url: mxchatChat.ajax_url,
1060 type: 'POST',
1061 data: {
1062 action: 'mxchat_check_new_messages',
1063 session_id: sessionId,
1064 last_seen_id: lastSeenMessageId,
1065 nonce: mxchatChat.nonce
1066 },
1067 success: function(response) {
1068 if (response.success && response.data.hasNewMessages) {
1069 showNotification();
1070 }
1071 }
1072 });
1073 }
1074
1075
1076 // ====================================
1077 // LIVE AGENT FUNCTIONALITY
1078 // ====================================
1079
1080 function updateChatModeIndicator(mode) {
1081 const indicator = document.getElementById('chat-mode-indicator');
1082 if (indicator) {
1083 // For Live Agent, keep as is; for AI mode, use the customized text
1084 if (mode === 'agent') {
1085 indicator.textContent = 'Live Agent';
1086 } else {
1087 // Get the custom AI agent text from a data attribute we'll add to the element
1088 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1089 indicator.textContent = customAiText;
1090 }
1091 }
1092 // Start or stop polling based on mode
1093 if (mode === 'agent') {
1094 startPolling();
1095 } else {
1096 stopPolling();
1097 }
1098 }
1099
1100 function startPolling() {
1101 // Clear any existing interval first
1102 stopPolling();
1103 // Start new polling interval
1104 pollingInterval = setInterval(checkForAgentMessages, 5000);
1105 //console.log("Started agent message polling");
1106 }
1107
1108 function stopPolling() {
1109 if (pollingInterval) {
1110 clearInterval(pollingInterval);
1111 pollingInterval = null;
1112 //console.log("Stopped agent message polling");
1113 }
1114 }
1115
1116 function checkForAgentMessages() {
1117 const sessionId = getChatSession();
1118 $.ajax({
1119 url: mxchatChat.ajax_url,
1120 type: 'POST',
1121 dataType: 'json',
1122 data: {
1123 action: 'mxchat_fetch_new_messages',
1124 session_id: sessionId,
1125 last_seen_id: lastSeenMessageId,
1126 persistence_enabled: 'true', // Add this too
1127 nonce: mxchatChat.nonce
1128 },
1129 success: function (response) {
1130 if (response.success && response.data?.new_messages) {
1131 let hasNewMessage = false;
1132
1133 response.data.new_messages.forEach(function (message) {
1134 if (message.role === "agent" && !processedMessageIds.has(message.id)) {
1135 hasNewMessage = true;
1136 // CHANGE THIS LINE:
1137 appendMessage("agent", message.content); // Instead of replaceLastMessage
1138 lastSeenMessageId = message.id;
1139 processedMessageIds.add(message.id);
1140 }
1141 });
1142
1143 if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1144 showNotification();
1145 }
1146
1147 scrollToBottom(true);
1148 }
1149 },
1150 error: function (xhr, status, error) {
1151 //console.error("Polling error:", xhr, status, error);
1152 }
1153 });
1154 }
1155
1156 // ====================================
1157 // CHAT HISTORY & PERSISTENCE
1158 // ====================================
1159
1160 function loadChatHistory() {
1161 var sessionId = getChatSession();
1162 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1163
1164 if (chatPersistenceEnabled && sessionId) {
1165 $.ajax({
1166 url: mxchatChat.ajax_url,
1167 type: 'POST',
1168 dataType: 'json',
1169 data: {
1170 action: 'mxchat_fetch_conversation_history',
1171 session_id: sessionId
1172 },
1173 success: function(response) {
1174 if (response.success && response.data && Array.isArray(response.data.conversation)) {
1175
1176
1177 var $chatBox = $('#chat-box');
1178 var $fragment = $(document.createDocumentFragment());
1179 let highestMessageId = lastSeenMessageId;
1180
1181 if (response.data.chat_mode) {
1182 updateChatModeIndicator(response.data.chat_mode);
1183 }
1184
1185 $.each(response.data.conversation, function(index, message) {
1186 // Skip agent messages if persistence is off
1187 if (!chatPersistenceEnabled && message.role === 'agent') {
1188 return;
1189 }
1190
1191 var messageClass, messageBgColor, messageFontColor;
1192
1193 switch (message.role) {
1194 case 'user':
1195 messageClass = 'user-message';
1196 messageBgColor = userMessageBgColor;
1197 messageFontColor = userMessageFontColor;
1198 break;
1199 case 'agent':
1200 messageClass = 'agent-message';
1201 messageBgColor = liveAgentMessageBgColor;
1202 messageFontColor = liveAgentMessageFontColor;
1203 break;
1204 default:
1205 messageClass = 'bot-message';
1206 messageBgColor = botMessageBgColor;
1207 messageFontColor = botMessageFontColor;
1208 break;
1209 }
1210
1211 var messageElement = $('<div>').addClass(messageClass)
1212 .css({
1213 'background': messageBgColor,
1214 'color': messageFontColor
1215 });
1216
1217 var content = message.content;
1218 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1219 content = decodeHTMLEntities(content);
1220
1221 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1222 messageElement.html(content);
1223 } else {
1224 var formattedContent = linkify(
1225 formatBoldText(
1226 convertNewlinesToBreaks(formatCodeBlocks(content))
1227 )
1228 );
1229 messageElement.html(formattedContent);
1230 }
1231
1232 $fragment.append(messageElement);
1233
1234 // In loadChatHistory, change this part:
1235 if (message.id) {
1236 highestMessageId = Math.max(highestMessageId, message.id);
1237 processedMessageIds.add(message.id); // Add all message IDs to processed set
1238 }
1239 });
1240
1241 $chatBox.append($fragment);
1242 scrollToBottom(true);
1243
1244 if (response.data.conversation.length > 0 && hasQuickQuestions()) {
1245 collapseQuickQuestions();
1246 }
1247
1248 // Update lastSeenMessageId after history loads
1249 lastSeenMessageId = highestMessageId;
1250
1251 // Only update chat mode if persistence is enabled
1252 if (chatPersistenceEnabled && response.data.conversation.length > 0) {
1253 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1254 if (lastMessage.role === 'agent') {
1255 updateChatModeIndicator('agent');
1256 }
1257 }
1258 } else {
1259 console.warn("No conversation history found.");
1260 }
1261 },
1262 error: function(xhr, status, error) {
1263 //console.error("Error loading chat history:", status, error);
1264 appendMessage("bot", "Unable to load chat history.");
1265 }
1266 });
1267 } else {
1268 console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
1269 }
1270 }
1271
1272
1273 // ====================================
1274 // FILE UPLOAD FUNCTIONALITY
1275 // ====================================
1276
1277 function addSafeEventListener(elementId, eventType, handler) {
1278 const element = document.getElementById(elementId);
1279 if (element) {
1280 element.addEventListener(eventType, handler);
1281 }
1282 }
1283
1284 function showActivePdf(filename) {
1285 const container = document.getElementById('active-pdf-container');
1286 const nameElement = document.getElementById('active-pdf-name');
1287
1288 if (!container || !nameElement) {
1289 //console.error('PDF container elements not found');
1290 return;
1291 }
1292
1293 nameElement.textContent = filename;
1294 container.style.display = 'flex';
1295 }
1296
1297 function showActiveWord(filename) {
1298 const container = document.getElementById('active-word-container');
1299 const nameElement = document.getElementById('active-word-name');
1300
1301 if (!container || !nameElement) {
1302 //console.error('Word document container elements not found');
1303 return;
1304 }
1305
1306 nameElement.textContent = filename;
1307 container.style.display = 'flex';
1308 }
1309
1310 function removeActivePdf() {
1311 const container = document.getElementById('active-pdf-container');
1312 const nameElement = document.getElementById('active-pdf-name');
1313
1314 if (!container || !nameElement || !activePdfFile) return;
1315
1316 fetch(mxchatChat.ajax_url, {
1317 method: 'POST',
1318 headers: {
1319 'Content-Type': 'application/x-www-form-urlencoded',
1320 },
1321 body: new URLSearchParams({
1322 'action': 'mxchat_remove_pdf',
1323 'session_id': sessionId,
1324 'nonce': mxchatChat.nonce
1325 })
1326 })
1327 .then(response => response.json())
1328 .then(data => {
1329 if (data.success) {
1330 container.style.display = 'none';
1331 nameElement.textContent = '';
1332 activePdfFile = null;
1333 appendMessage('bot', 'PDF removed.');
1334 }
1335 })
1336 .catch(error => {
1337 //console.error('Error removing PDF:', error);
1338 });
1339 }
1340
1341 function removeActiveWord() {
1342 const container = document.getElementById('active-word-container');
1343 const nameElement = document.getElementById('active-word-name');
1344
1345 if (!container || !nameElement || !activeWordFile) return;
1346
1347 fetch(mxchatChat.ajax_url, {
1348 method: 'POST',
1349 headers: {
1350 'Content-Type': 'application/x-www-form-urlencoded',
1351 },
1352 body: new URLSearchParams({
1353 'action': 'mxchat_remove_word',
1354 'session_id': sessionId,
1355 'nonce': mxchatChat.nonce
1356 })
1357 })
1358 .then(response => response.json())
1359 .then(data => {
1360 if (data.success) {
1361 container.style.display = 'none';
1362 nameElement.textContent = '';
1363 activeWordFile = null;
1364 appendMessage('bot', 'Word document removed.');
1365 }
1366 })
1367 .catch(error => {
1368 //console.error('Error removing Word document:', error);
1369 });
1370 }
1371
1372 // ====================================
1373 // CONSENT & COMPLIANCE (GDPR)
1374 // ====================================
1375
1376 function initializeChatVisibility() {
1377 //console.log('Initializing chat visibility');
1378 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1379 mxchatChat.complianz_toggle === '1' ||
1380 mxchatChat.complianz_toggle === 1;
1381
1382 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1383 // Initial check
1384 checkConsentAndShowChat();
1385
1386 // Listen for consent changes
1387 $(document).on('cmplz_status_change', function(event) {
1388 //console.log('Status change detected');
1389 checkConsentAndShowChat();
1390 });
1391 } else {
1392 // If Complianz is not enabled, always show
1393 $('#floating-chatbot-button')
1394 .css('display', 'flex')
1395 .removeClass('hidden no-consent')
1396 .fadeTo(500, 1);
1397
1398 // Also check pre-chat message when Complianz is not enabled
1399 checkPreChatDismissal();
1400 }
1401 }
1402
1403
1404 function checkConsentAndShowChat() {
1405 var consentStatus = cmplz_has_consent('marketing');
1406 var consentType = complianz.consenttype;
1407
1408 //console.log('Checking consent:', {status: consentStatus,type: consentType});
1409
1410 let $widget = $('#floating-chatbot-button');
1411 let $chatbot = $('#floating-chatbot');
1412 let $preChat = $('#pre-chat-message');
1413
1414 if (consentStatus === true) {
1415 //console.log('Consent granted - showing widget');
1416 $widget
1417 .removeClass('no-consent')
1418 .css('display', 'flex')
1419 .removeClass('hidden')
1420 .fadeTo(500, 1);
1421 $chatbot.removeClass('no-consent');
1422
1423 // Show pre-chat message if not dismissed
1424 checkPreChatDismissal();
1425 } else {
1426 //console.log('No consent - hiding widget');
1427 $widget
1428 .addClass('no-consent')
1429 .fadeTo(500, 0, function() {
1430 $(this)
1431 .css('display', 'none')
1432 .addClass('hidden');
1433 });
1434 $chatbot.addClass('no-consent');
1435
1436 // Hide pre-chat message when no consent
1437 $preChat.hide();
1438 }
1439 }
1440
1441
1442 // ====================================
1443 // PRE-CHAT MESSAGE HANDLING
1444 // ====================================
1445
1446 function checkPreChatDismissal() {
1447 $.ajax({
1448 url: mxchatChat.ajax_url,
1449 type: 'POST',
1450 data: {
1451 action: 'mxchat_check_pre_chat_message_status',
1452 _ajax_nonce: mxchatChat.nonce
1453 },
1454 success: function(response) {
1455 if (response.success && !response.data.dismissed) {
1456 $('#pre-chat-message').fadeIn(250);
1457 } else {
1458 $('#pre-chat-message').hide();
1459 }
1460 },
1461 error: function() {
1462 //console.error('Failed to check pre-chat message dismissal status.');
1463 }
1464 });
1465 }
1466
1467 function handlePreChatDismissal() {
1468 $('#pre-chat-message').fadeOut(200);
1469 $.ajax({
1470 url: mxchatChat.ajax_url,
1471 type: 'POST',
1472 data: {
1473 action: 'mxchat_dismiss_pre_chat_message',
1474 _ajax_nonce: mxchatChat.nonce
1475 },
1476 success: function() {
1477 $('#pre-chat-message').hide();
1478 },
1479 error: function() {
1480 //console.error('Failed to dismiss pre-chat message.');
1481 }
1482 });
1483 }
1484
1485
1486 // ====================================
1487 // UTILITY FUNCTIONS
1488 // ====================================
1489
1490 function copyToClipboard(text) {
1491 var tempInput = $('<input>');
1492 $('body').append(tempInput);
1493 tempInput.val(text).select();
1494 document.execCommand('copy');
1495 tempInput.remove();
1496 }
1497
1498
1499 function isImageHtml(str) {
1500 return str.startsWith('<img') && str.endsWith('>');
1501 }
1502
1503
1504 // ====================================
1505 // EVENT HANDLERS & INITIALIZATION
1506 // ====================================
1507
1508 $(document).on('click', '.mxchat-popular-question', function () {
1509 var question = $(this).text();
1510
1511 // Append the question as if the user typed it
1512 appendMessage("user", question);
1513
1514 // Only collapse if there are questions
1515 if (hasQuickQuestions()) {
1516 collapseQuickQuestions();
1517 }
1518
1519 // Send the question to the server
1520 sendMessageToChatbot(question);
1521 });
1522
1523 $(document).on('click', '.questions-toggle-btn', function(e) {
1524 e.preventDefault();
1525 e.stopPropagation();
1526 expandQuickQuestions();
1527 });
1528
1529 $(document).on('click', '.questions-collapse-btn', function(e) {
1530 e.preventDefault();
1531 e.stopPropagation();
1532 collapseQuickQuestions();
1533 });
1534
1535 // Chatbot visibility toggle handlers
1536 $(document).on('click', '#floating-chatbot-button', function() {
1537 var chatbot = $('#floating-chatbot');
1538 if (chatbot.hasClass('hidden')) {
1539 chatbot.removeClass('hidden').addClass('visible');
1540 $(this).addClass('hidden');
1541 $('#chat-notification-badge').hide(); // Hide notification when opening chat
1542 disableScroll();
1543 $('#pre-chat-message').fadeOut(250);
1544 } else {
1545 chatbot.removeClass('visible').addClass('hidden');
1546 $(this).removeClass('hidden');
1547 enableScroll();
1548 checkPreChatDismissal();
1549 }
1550 });
1551
1552 $(document).on('click', '#exit-chat-button', function() {
1553 $('#floating-chatbot').addClass('hidden').removeClass('visible');
1554 $('#floating-chatbot-button').removeClass('hidden');
1555 enableScroll();
1556 });
1557
1558 $(document).on('click', '.close-pre-chat-message', function(e) {
1559 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
1560 $('#pre-chat-message').fadeOut(200, function() {
1561 $(this).remove();
1562 });
1563 });
1564
1565 // Add to Cart button handler
1566 $(document).on('click', '.mxchat-add-to-cart-button', function() {
1567 var productId = $(this).data('product-id');
1568
1569 // Get the button text instead of hardcoded "add to cart"
1570 var buttonText = $(this).text() || "add to cart";
1571
1572 // Add a special prefix to indicate this is from button
1573 appendMessage("user", buttonText);
1574 sendMessageToChatbot("!addtocart"); // Special command to indicate button click
1575 });
1576
1577 // PDF upload button handlers
1578 if (document.getElementById('pdf-upload-btn')) {
1579 document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1580 document.getElementById('pdf-upload').click();
1581 });
1582 }
1583
1584 // Word upload button handlers
1585 if (document.getElementById('word-upload-btn')) {
1586 document.getElementById('word-upload-btn').addEventListener('click', function() {
1587 document.getElementById('word-upload').click();
1588 });
1589 }
1590
1591 // PDF file input change handler
1592 addSafeEventListener('pdf-upload', 'change', async function(e) {
1593 const file = e.target.files[0];
1594
1595 if (!file || file.type !== 'application/pdf') {
1596 alert('Please select a valid PDF file.');
1597 return;
1598 }
1599
1600 if (!sessionId) {
1601 //console.error('No session ID found');
1602 alert('Error: No session ID found');
1603 return;
1604 }
1605
1606 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1607 //console.error('mxchatChat not properly configured:', mxchatChat);
1608 alert('Error: Ajax configuration missing');
1609 return;
1610 }
1611
1612 // Disable buttons and show loading state
1613 const uploadBtn = document.getElementById('pdf-upload-btn');
1614 const sendBtn = document.getElementById('send-button');
1615 const originalBtnContent = uploadBtn.innerHTML;
1616
1617 try {
1618 const formData = new FormData();
1619 formData.append('action', 'mxchat_upload_pdf');
1620 formData.append('pdf_file', file);
1621 formData.append('session_id', sessionId);
1622 formData.append('nonce', mxchatChat.nonce);
1623
1624 uploadBtn.disabled = true;
1625 sendBtn.disabled = true;
1626 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1627 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1628 </svg>`;
1629
1630 const response = await fetch(mxchatChat.ajax_url, {
1631 method: 'POST',
1632 body: formData
1633 });
1634
1635 const data = await response.json();
1636
1637 if (data.success) {
1638 // Hide popular questions if they exist
1639 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1640 if (hasQuickQuestions()) {
1641 collapseQuickQuestions();
1642 }
1643
1644 // Show the active PDF name
1645 showActivePdf(data.data.filename);
1646
1647 appendMessage('bot', data.data.message);
1648 scrollToBottom();
1649 activePdfFile = data.data.filename;
1650 } else {
1651 //console.error('Upload failed:', data.data);
1652 alert('Failed to upload PDF. Please try again.');
1653 }
1654 } catch (error) {
1655 //console.error('Upload error:', error);
1656 alert('Error uploading file. Please try again.');
1657 } finally {
1658 uploadBtn.disabled = false;
1659 sendBtn.disabled = false;
1660 uploadBtn.innerHTML = originalBtnContent;
1661 this.value = ''; // Reset file input
1662 }
1663 });
1664
1665 // Word file input change handler
1666 addSafeEventListener('word-upload', 'change', async function(e) {
1667 const file = e.target.files[0];
1668
1669 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
1670 alert('Please select a valid Word document (.docx).');
1671 return;
1672 }
1673
1674 if (!sessionId) {
1675 //console.error('No session ID found');
1676 alert('Error: No session ID found');
1677 return;
1678 }
1679
1680 // Disable buttons and show loading state
1681 const uploadBtn = document.getElementById('word-upload-btn');
1682 const sendBtn = document.getElementById('send-button');
1683 const originalBtnContent = uploadBtn.innerHTML;
1684
1685 try {
1686 const formData = new FormData();
1687 formData.append('action', 'mxchat_upload_word');
1688 formData.append('word_file', file);
1689 formData.append('session_id', sessionId);
1690 formData.append('nonce', mxchatChat.nonce);
1691
1692 uploadBtn.disabled = true;
1693 sendBtn.disabled = true;
1694 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1695 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1696 </svg>`;
1697
1698 const response = await fetch(mxchatChat.ajax_url, {
1699 method: 'POST',
1700 body: formData
1701 });
1702
1703 const data = await response.json();
1704
1705 if (data.success) {
1706 // Hide popular questions if they exist
1707 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1708 if (hasQuickQuestions()) {
1709 collapseQuickQuestions();
1710 }
1711
1712 // Show the active Word document name
1713 showActiveWord(data.data.filename);
1714
1715 appendMessage('bot', data.data.message);
1716 scrollToBottom();
1717 activeWordFile = data.data.filename;
1718 } else {
1719 //console.error('Upload failed:', data.data);
1720 alert('Failed to upload Word document. Please try again.');
1721 }
1722 } catch (error) {
1723 //console.error('Upload error:', error);
1724 alert('Error uploading file. Please try again.');
1725 } finally {
1726 uploadBtn.disabled = false;
1727 sendBtn.disabled = false;
1728 uploadBtn.innerHTML = originalBtnContent;
1729 this.value = ''; // Reset file input
1730 }
1731 });
1732
1733 // Remove button click handlers
1734 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
1735 e.preventDefault();
1736 e.stopPropagation();
1737 removeActivePdf();
1738 });
1739
1740 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
1741 e.preventDefault();
1742 e.stopPropagation();
1743 removeActiveWord();
1744 });
1745
1746 // Window resize handlers
1747 $(window).on('resize orientationchange', function() {
1748 setFullHeight();
1749 });
1750
1751
1752 // ====================================
1753 // TOOLBAR & STYLING SETUP
1754 // ====================================
1755
1756 // Apply toolbar settings
1757 if (mxchatChat.chat_toolbar_toggle === 'on') {
1758 $('.chat-toolbar').show();
1759 } else {
1760 $('.chat-toolbar').hide();
1761 }
1762
1763 // Apply toolbar icon colors
1764 const toolbarElements = [
1765 '#mxchat-chatbot .toolbar-btn svg',
1766 '#mxchat-chatbot .active-pdf-name',
1767 '#mxchat-chatbot .active-word-name',
1768 '#mxchat-chatbot .remove-pdf-btn svg',
1769 '#mxchat-chatbot .remove-word-btn svg',
1770 '#mxchat-chatbot .toolbar-perplexity svg'
1771 ];
1772
1773 toolbarElements.forEach(selector => {
1774 $(selector).css({
1775 'fill': toolbarIconColor,
1776 'stroke': toolbarIconColor,
1777 'color': toolbarIconColor
1778 });
1779 });
1780
1781
1782 // ====================================
1783 // EMAIL COLLECTION SETUP
1784 // ====================================
1785
1786 // Email collection form setup and handlers
1787 const emailForm = document.getElementById('email-collection-form');
1788 const emailBlocker = document.getElementById('email-blocker');
1789 const chatbotWrapper = document.getElementById('chat-container');
1790
1791 if (emailForm && emailBlocker && chatbotWrapper) {
1792 // Check if email exists for the current session
1793 function checkSessionAndEmail() {
1794 const sessionId = getChatSession();
1795 //console.log("[DEBUG JS] checkSessionAndEmail -> sessionId:", sessionId);
1796
1797 fetch(mxchatChat.ajax_url, {
1798 method: 'POST',
1799 headers: {
1800 'Content-Type': 'application/x-www-form-urlencoded',
1801 },
1802 body: new URLSearchParams({
1803 action: 'mxchat_check_email_provided',
1804 session_id: sessionId,
1805 nonce: mxchatChat.nonce,
1806 }),
1807 })
1808 .then((response) => response.json())
1809 .then((data) => {
1810 //console.log("[DEBUG JS] mxchat_check_email_provided response:", data);
1811
1812 if (data.success) {
1813 if (data.data.logged_in) {
1814 //console.log("[DEBUG JS] User is logged in. Hiding email form.");
1815 emailBlocker.style.display = 'none';
1816 chatbotWrapper.style.display = 'flex';
1817 } else if (data.data.email) {
1818 //console.log("[DEBUG JS] Email found for session. Hiding email form.");
1819 emailBlocker.style.display = 'none';
1820 chatbotWrapper.style.display = 'flex';
1821 } else {
1822 //console.log("[DEBUG JS] No email provided. Showing email form.");
1823 emailBlocker.style.display = 'flex';
1824 chatbotWrapper.style.display = 'none';
1825 }
1826 } else {
1827 //console.log("[DEBUG JS] Error or no data received. Showing email form.");
1828 emailBlocker.style.display = 'flex';
1829 chatbotWrapper.style.display = 'none';
1830 }
1831 })
1832 .catch((error) => {
1833 // //console.error("[DEBUG JS] Fetch error -> forcing email form visible:", error);
1834 emailBlocker.style.display = 'flex';
1835 chatbotWrapper.style.display = 'none';
1836 });
1837 }
1838
1839 // Handle email form submission
1840 emailForm.addEventListener('submit', function (event) {
1841 event.preventDefault();
1842 const userEmail = document.getElementById('user-email').value;
1843 const sessionId = getChatSession();
1844
1845 if (userEmail) {
1846 fetch(mxchatChat.ajax_url, {
1847 method: 'POST',
1848 headers: {
1849 'Content-Type': 'application/x-www-form-urlencoded',
1850 },
1851 body: new URLSearchParams({
1852 action: 'mxchat_handle_save_email_and_response',
1853 email: userEmail,
1854 session_id: sessionId,
1855 nonce: mxchatChat.nonce,
1856 }),
1857 })
1858 .then((response) => response.json())
1859 .then((data) => {
1860 //console.log('Backend response:', data);
1861 if (data.success) {
1862 //console.log('Email saved successfully:', userEmail);
1863 emailBlocker.style.display = 'none';
1864 chatbotWrapper.style.display = 'flex';
1865
1866 // Optionally handle bot response
1867 if (data.message) {
1868 appendMessage('bot', data.message);
1869 scrollToBottom();
1870 }
1871 } else {
1872 //console.error('Error saving email:', data.message || 'Unknown error');
1873 }
1874 })
1875 .catch((error) => {
1876 //console.error('AJAX error:', error);
1877 });
1878 }
1879 });
1880
1881 // Check session and email status on page load
1882 checkSessionAndEmail();
1883 } else if (mxchatChat.email_collection_enabled) {
1884 // Only show error if email collection is enabled but elements are missing
1885 //console.error('Essential elements for email handling are missing.');
1886 }
1887
1888 // Open chatbot when pre-chat message is clicked
1889 $(document).on('click', '#pre-chat-message', function() {
1890 var chatbot = $('#floating-chatbot');
1891 if (chatbot.hasClass('hidden')) {
1892 chatbot.removeClass('hidden').addClass('visible');
1893 $('#floating-chatbot-button').addClass('hidden');
1894 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
1895 disableScroll(); // Disable scroll when chatbot opens
1896 }
1897 });
1898
1899 var closeButton = document.querySelector('.close-pre-chat-message');
1900 if (closeButton) {
1901 closeButton.addEventListener('click', function() {
1902 $('#pre-chat-message').fadeOut(200); // Hide the message
1903
1904 // Send an AJAX request to set the transient flag for 24 hours
1905 $.ajax({
1906 url: mxchatChat.ajax_url,
1907 type: 'POST',
1908 data: {
1909 action: 'mxchat_dismiss_pre_chat_message',
1910 _ajax_nonce: mxchatChat.nonce
1911 },
1912 success: function() {
1913 //console.log('Pre-chat message dismissed for 24 hours.');
1914
1915 // Ensure the message is hidden after dismissal
1916 $('#pre-chat-message').hide();
1917 },
1918 error: function() {
1919 ////console.error('Failed to dismiss pre-chat message.');
1920 }
1921 });
1922 });
1923 }
1924
1925
1926 function hasQuickQuestions() {
1927 const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
1928 return questionButtons.length > 0;
1929 }
1930
1931 function collapseQuickQuestions() {
1932 const questionsContainer = document.getElementById('mxchat-popular-questions');
1933 if (questionsContainer && hasQuickQuestions()) {
1934 questionsContainer.classList.add('collapsed');
1935 questionsContainer.classList.add('has-been-collapsed');
1936 try {
1937 sessionStorage.setItem('mxchat_questions_collapsed', 'true');
1938 sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
1939 } catch (e) {
1940 // Ignore if sessionStorage is not available
1941 }
1942 }
1943 }
1944
1945 function expandQuickQuestions() {
1946 const questionsContainer = document.getElementById('mxchat-popular-questions');
1947 if (questionsContainer && hasQuickQuestions()) {
1948 questionsContainer.classList.remove('collapsed');
1949 try {
1950 sessionStorage.setItem('mxchat_questions_collapsed', 'false');
1951 } catch (e) {
1952 // Ignore if sessionStorage is not available
1953 }
1954 }
1955 }
1956
1957 function checkQuickQuestionsState() {
1958 if (!hasQuickQuestions()) {
1959 return; // Don't do anything if no questions exist
1960 }
1961
1962 try {
1963 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
1964 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
1965
1966 const questionsContainer = document.getElementById('mxchat-popular-questions');
1967 if (questionsContainer) {
1968 if (hasBeenCollapsed === 'true') {
1969 questionsContainer.classList.add('has-been-collapsed');
1970 }
1971 if (isCollapsed === 'true') {
1972 questionsContainer.classList.add('collapsed');
1973 }
1974 }
1975 } catch (e) {
1976 // Ignore if sessionStorage is not available
1977 }
1978 }
1979
1980
1981
1982
1983 // ====================================
1984 // MAIN INITIALIZATION
1985 // ====================================
1986
1987 if ($('#floating-chatbot').hasClass('hidden')) {
1988 $('#floating-chatbot-button').removeClass('hidden');
1989 }
1990 // Initialize when document is ready
1991 setFullHeight();
1992 initializeChatVisibility();
1993 loadChatHistory();
1994
1995 }); // End of jQuery ready
1996
1997 // ====================================
1998 // GLOBAL EVENT LISTENERS (Outside jQuery)
1999 // ====================================
2000
2001 // Event listener for copy button (code blocks)
2002 document.addEventListener("click", (e) => {
2003 if (e.target.classList.contains("mxchat-copy-button")) {
2004 const copyButton = e.target;
2005 const codeBlock = copyButton
2006 .closest(".mxchat-code-block-container")
2007 .querySelector(".mxchat-code-block code");
2008
2009 if (codeBlock) {
2010 // Preserve formatting using innerText
2011 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
2012 copyButton.textContent = "Copied!";
2013 copyButton.setAttribute("aria-label", "Copied to clipboard");
2014
2015 setTimeout(() => {
2016 copyButton.textContent = "Copy";
2017 copyButton.setAttribute("aria-label", "Copy to clipboard");
2018 }, 2000);
2019 });
2020 }
2021 }
2022 });
2023
2024
2025
2026
2027
2028
2029