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

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