PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.6
MxChat – AI Chatbot & Content Generation for WordPress v2.3.6
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / chat-script.js

chat-script.js in MxChat – AI Chatbot & Content Generation for WordPress 2.3.6, at js/chat-script.js

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