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

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