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

2,650 lines 98.5 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 // EMAIL COLLECTION SETUP
2040 // ====================================
2041 // Email collection form setup and handlers
2042 const emailForm = document.getElementById('email-collection-form');
2043 const emailBlocker = document.getElementById('email-blocker');
2044 const chatbotWrapper = document.getElementById('chat-container');
2045
2046 if (emailForm && emailBlocker && chatbotWrapper) {
2047
2048 // NEW: Check if server already provided the email state
2049 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2050 console.log('Using server-provided email state:', mxchatChat.initial_email_state);
2051
2052 // Use server-provided state instead of making AJAX call
2053 const emailState = mxchatChat.initial_email_state;
2054 if (emailState.show_email_form) {
2055 showEmailForm();
2056 } else {
2057 showChatContainer();
2058 }
2059
2060 // Continue to set up form handlers but skip the AJAX check
2061 setupFormHandlers();
2062
2063 } else {
2064 // FALLBACK: Use existing AJAX approach if server state not available
2065 console.log('Server state not available, using AJAX fallback');
2066 setupFormHandlers();
2067 // Initialize email check with delay to prevent race conditions
2068 setTimeout(checkSessionAndEmail, 100);
2069 }
2070
2071 // Add loading state management
2072 let isSubmitting = false;
2073
2074 // Check if email exists for the current session
2075 function checkSessionAndEmail() {
2076 const sessionId = getChatSession();
2077
2078 // Add timeout to prevent hanging
2079 const controller = new AbortController();
2080 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
2081
2082 fetch(mxchatChat.ajax_url, {
2083 method: 'POST',
2084 headers: {
2085 'Content-Type': 'application/x-www-form-urlencoded',
2086 },
2087 body: new URLSearchParams({
2088 action: 'mxchat_check_email_provided',
2089 session_id: sessionId,
2090 nonce: mxchatChat.nonce,
2091 }),
2092 signal: controller.signal
2093 })
2094 .then((response) => {
2095 clearTimeout(timeoutId);
2096 if (!response.ok) {
2097 throw new Error(`HTTP error! status: ${response.status}`);
2098 }
2099 return response.json();
2100 })
2101 .then((data) => {
2102 if (data.success) {
2103 if (data.data.logged_in || data.data.email) {
2104 showChatContainer();
2105 } else {
2106 showEmailForm();
2107 }
2108 } else {
2109 // On error, default to showing email form
2110 showEmailForm();
2111 }
2112 })
2113 .catch((error) => {
2114 clearTimeout(timeoutId);
2115 console.warn('Email check failed, defaulting to email form:', error);
2116 showEmailForm();
2117 });
2118 }
2119
2120 // Optimized UI transition functions
2121 function showEmailForm() {
2122 emailBlocker.style.display = 'flex';
2123 chatbotWrapper.style.display = 'none';
2124 }
2125
2126 function showChatContainer() {
2127 // Show chat immediately without delay
2128 emailBlocker.style.display = 'none';
2129 chatbotWrapper.style.display = 'flex';
2130
2131 // Load chat history only after showing chat container
2132 if (typeof loadChatHistory === 'function') {
2133 loadChatHistory();
2134 }
2135 }
2136
2137 // Enhanced email validation
2138 function isValidEmail(email) {
2139 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2140 return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2141 }
2142
2143 // Enhanced name validation
2144 function isValidName(name) {
2145 return name && name.trim().length >= 2 && name.trim().length <= 100;
2146 }
2147
2148 // Show loading state with spinner
2149 function setSubmissionState(loading) {
2150 const submitButton = document.getElementById('email-submit-button');
2151 const emailInput = document.getElementById('user-email');
2152 const nameInput = document.getElementById('user-name');
2153
2154 if (loading) {
2155 isSubmitting = true;
2156 submitButton.disabled = true;
2157 emailInput.disabled = true;
2158 if (nameInput) nameInput.disabled = true;
2159
2160 // Store original content and add spinner
2161 if (!submitButton.getAttribute('data-original-html')) {
2162 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2163 }
2164
2165 // Add loading spinner while keeping original text
2166 const originalText = submitButton.textContent;
2167 submitButton.innerHTML = `
2168 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2169 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2170 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2171 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2172 </circle>
2173 </svg>
2174 ${originalText}
2175 `;
2176
2177 submitButton.style.opacity = '0.8';
2178 } else {
2179 isSubmitting = false;
2180 submitButton.disabled = false;
2181 emailInput.disabled = false;
2182 if (nameInput) nameInput.disabled = false;
2183
2184 // Restore original content
2185 const originalHtml = submitButton.getAttribute('data-original-html');
2186 if (originalHtml) {
2187 submitButton.innerHTML = originalHtml;
2188 }
2189
2190 submitButton.style.opacity = '1';
2191 }
2192 }
2193
2194 // Error display functions
2195 function showEmailError(message) {
2196 clearEmailError();
2197
2198 const errorDiv = document.createElement('div');
2199 errorDiv.className = 'email-error';
2200 errorDiv.style.cssText = `
2201 color: #e74c3c;
2202 font-size: 12px;
2203 margin-top: 8px;
2204 padding: 4px 0;
2205 animation: fadeInError 0.3s ease;
2206 `;
2207 errorDiv.textContent = message;
2208
2209 // Add CSS animation if not already present
2210 if (!document.getElementById('email-error-styles')) {
2211 const style = document.createElement('style');
2212 style.id = 'email-error-styles';
2213 style.textContent = `
2214 @keyframes fadeInError {
2215 from { opacity: 0; transform: translateY(-5px); }
2216 to { opacity: 1; transform: translateY(0); }
2217 }
2218 .email-input-shake {
2219 animation: shake 0.5s ease-in-out;
2220 }
2221 @keyframes shake {
2222 0%, 100% { transform: translateX(0); }
2223 25% { transform: translateX(-5px); }
2224 75% { transform: translateX(5px); }
2225 }
2226 `;
2227 document.head.appendChild(style);
2228 }
2229
2230 emailForm.appendChild(errorDiv);
2231
2232 // Add shake animation to inputs
2233 const emailInput = document.getElementById('user-email');
2234 const nameInput = document.getElementById('user-name');
2235
2236 if (emailInput) {
2237 emailInput.classList.add('email-input-shake');
2238 setTimeout(() => {
2239 emailInput.classList.remove('email-input-shake');
2240 }, 500);
2241 }
2242
2243 if (nameInput) {
2244 nameInput.classList.add('email-input-shake');
2245 setTimeout(() => {
2246 nameInput.classList.remove('email-input-shake');
2247 }, 500);
2248 }
2249 }
2250
2251 function clearEmailError() {
2252 const existingErrors = emailForm.querySelectorAll('.email-error');
2253 existingErrors.forEach(error => error.remove());
2254 }
2255
2256 // NEW: Separate function to set up all form handlers
2257 function setupFormHandlers() {
2258 // Add CSS for spinner animation if not already present
2259 if (!document.getElementById('email-spinner-styles')) {
2260 const style = document.createElement('style');
2261 style.id = 'email-spinner-styles';
2262 style.textContent = `
2263 @keyframes spin {
2264 from { transform: rotate(0deg); }
2265 to { transform: rotate(360deg); }
2266 }
2267 .email-spinner {
2268 display: inline-block;
2269 vertical-align: middle;
2270 }
2271 `;
2272 document.head.appendChild(style);
2273 }
2274
2275 // Handle email form submission with improved error handling
2276 emailForm.addEventListener('submit', function (event) {
2277 event.preventDefault();
2278
2279 // Prevent double submission
2280 if (isSubmitting) {
2281 return;
2282 }
2283
2284 const userEmail = document.getElementById('user-email').value.trim();
2285 const nameInput = document.getElementById('user-name');
2286 const userName = nameInput ? nameInput.value.trim() : '';
2287 const sessionId = getChatSession();
2288
2289 // Validate email before submission
2290 if (!userEmail) {
2291 showEmailError('Please enter your email address.');
2292 return;
2293 }
2294
2295 if (!isValidEmail(userEmail)) {
2296 showEmailError('Please enter a valid email address.');
2297 return;
2298 }
2299
2300 // Validate name if field exists
2301 if (nameInput && !isValidName(userName)) {
2302 showEmailError('Please enter a valid name (2-100 characters).');
2303 return;
2304 }
2305
2306 // Clear any existing errors
2307 clearEmailError();
2308 setSubmissionState(true);
2309
2310 // Add timeout for submission
2311 const controller = new AbortController();
2312 const timeoutId = setTimeout(() => {
2313 controller.abort();
2314 setSubmissionState(false);
2315 showEmailError('Request timed out. Please try again.');
2316 }, 15000); // 15 second timeout
2317
2318 // Prepare form data with optional name
2319 const formData = new URLSearchParams({
2320 action: 'mxchat_handle_save_email_and_response',
2321 email: userEmail,
2322 session_id: sessionId,
2323 nonce: mxchatChat.nonce,
2324 });
2325
2326 // Add name to form data if provided
2327 if (userName) {
2328 formData.append('name', userName);
2329 }
2330
2331 fetch(mxchatChat.ajax_url, {
2332 method: 'POST',
2333 headers: {
2334 'Content-Type': 'application/x-www-form-urlencoded',
2335 },
2336 body: formData,
2337 signal: controller.signal
2338 })
2339 .then((response) => {
2340 clearTimeout(timeoutId);
2341 if (!response.ok) {
2342 throw new Error(`HTTP error! status: ${response.status}`);
2343 }
2344 return response.json();
2345 })
2346 .then((data) => {
2347 setSubmissionState(false);
2348
2349 if (data.success) {
2350 // Show chat immediately
2351 showChatContainer();
2352
2353 // Handle bot response if provided
2354 if (data.message && typeof appendMessage === 'function') {
2355 setTimeout(() => {
2356 appendMessage('bot', data.message);
2357 if (typeof scrollToBottom === 'function') {
2358 scrollToBottom();
2359 }
2360 }, 100);
2361 }
2362 } else {
2363 showEmailError(data.message || 'Failed to save email. Please try again.');
2364 }
2365 })
2366 .catch((error) => {
2367 clearTimeout(timeoutId);
2368 setSubmissionState(false);
2369
2370 if (error.name === 'AbortError') {
2371 showEmailError('Request timed out. Please try again.');
2372 } else {
2373 console.error('Email submission error:', error);
2374 showEmailError('An error occurred. Please try again.');
2375 }
2376 });
2377 });
2378
2379 // Real-time email validation
2380 const emailInput = document.getElementById('user-email');
2381 if (emailInput) {
2382 let validationTimeout;
2383
2384 emailInput.addEventListener('input', function() {
2385 // Clear previous validation timeout
2386 if (validationTimeout) {
2387 clearTimeout(validationTimeout);
2388 }
2389
2390 // Debounce validation
2391 validationTimeout = setTimeout(() => {
2392 const email = this.value.trim();
2393 clearEmailError();
2394
2395 if (email && !isValidEmail(email)) {
2396 showEmailError('Please enter a valid email address.');
2397 }
2398 }, 500);
2399 });
2400
2401 // Handle Enter key
2402 emailInput.addEventListener('keypress', function(e) {
2403 if (e.key === 'Enter' && !isSubmitting) {
2404 emailForm.dispatchEvent(new Event('submit'));
2405 }
2406 });
2407 }
2408
2409 // Real-time name validation
2410 const nameInput = document.getElementById('user-name');
2411 if (nameInput) {
2412 let nameValidationTimeout;
2413
2414 nameInput.addEventListener('input', function() {
2415 // Clear previous validation timeout
2416 if (nameValidationTimeout) {
2417 clearTimeout(nameValidationTimeout);
2418 }
2419
2420 // Debounce validation
2421 nameValidationTimeout = setTimeout(() => {
2422 const name = this.value.trim();
2423 clearEmailError();
2424
2425 if (name && !isValidName(name)) {
2426 showEmailError('Name must be between 2 and 100 characters.');
2427 }
2428 }, 500);
2429 });
2430
2431 // Handle Enter key
2432 nameInput.addEventListener('keypress', function(e) {
2433 if (e.key === 'Enter' && !isSubmitting) {
2434 emailForm.dispatchEvent(new Event('submit'));
2435 }
2436 });
2437 }
2438 }
2439
2440 } else if (mxchatChat && mxchatChat.email_collection_enabled) {
2441 console.error('Essential elements for email handling are missing:', {
2442 emailForm: !!emailForm,
2443 emailBlocker: !!emailBlocker,
2444 chatbotWrapper: !!chatbotWrapper
2445 });
2446 }
2447
2448
2449 // Open chatbot when pre-chat message is clicked
2450 $(document).on('click', '#pre-chat-message', function() {
2451 var chatbot = $('#floating-chatbot');
2452 if (chatbot.hasClass('hidden')) {
2453 chatbot.removeClass('hidden').addClass('visible');
2454 $('#floating-chatbot-button').addClass('hidden');
2455 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
2456 disableScroll(); // Disable scroll when chatbot opens
2457 }
2458 });
2459
2460 var closeButton = document.querySelector('.close-pre-chat-message');
2461 if (closeButton) {
2462 closeButton.addEventListener('click', function() {
2463 $('#pre-chat-message').fadeOut(200); // Hide the message
2464
2465 // Send an AJAX request to set the transient flag for 24 hours
2466 $.ajax({
2467 url: mxchatChat.ajax_url,
2468 type: 'POST',
2469 data: {
2470 action: 'mxchat_dismiss_pre_chat_message',
2471 _ajax_nonce: mxchatChat.nonce
2472 },
2473 success: function() {
2474 //console.log('Pre-chat message dismissed for 24 hours.');
2475
2476 // Ensure the message is hidden after dismissal
2477 $('#pre-chat-message').hide();
2478 },
2479 error: function() {
2480 ////console.error('Failed to dismiss pre-chat message.');
2481 }
2482 });
2483 });
2484 }
2485
2486
2487 function hasQuickQuestions() {
2488 const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
2489 return questionButtons.length > 0;
2490 }
2491
2492 function collapseQuickQuestions() {
2493 const questionsContainer = document.getElementById('mxchat-popular-questions');
2494 if (questionsContainer && hasQuickQuestions()) {
2495 questionsContainer.classList.add('collapsed');
2496 questionsContainer.classList.add('has-been-collapsed');
2497 try {
2498 sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2499 sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
2500 } catch (e) {
2501 // Ignore if sessionStorage is not available
2502 }
2503 }
2504 }
2505
2506 function expandQuickQuestions() {
2507 const questionsContainer = document.getElementById('mxchat-popular-questions');
2508 if (questionsContainer && hasQuickQuestions()) {
2509 questionsContainer.classList.remove('collapsed');
2510 try {
2511 sessionStorage.setItem('mxchat_questions_collapsed', 'false');
2512 } catch (e) {
2513 // Ignore if sessionStorage is not available
2514 }
2515 }
2516 }
2517
2518 function checkQuickQuestionsState() {
2519 if (!hasQuickQuestions()) {
2520 return; // Don't do anything if no questions exist
2521 }
2522
2523 try {
2524 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2525 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2526
2527 const questionsContainer = document.getElementById('mxchat-popular-questions');
2528 if (questionsContainer) {
2529 if (hasBeenCollapsed === 'true') {
2530 questionsContainer.classList.add('has-been-collapsed');
2531 }
2532 if (isCollapsed === 'true') {
2533 questionsContainer.classList.add('collapsed');
2534 }
2535 }
2536 } catch (e) {
2537 // Ignore if sessionStorage is not available
2538 }
2539 }
2540
2541 // NEW: Global delegation for dynamically added links as fallback
2542 $(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
2543 const $link = $(this);
2544 const messageDiv = $link.closest('.bot-message, .agent-message');
2545
2546 // Only process bot/agent message links
2547 if (messageDiv.length > 0) {
2548 const originalHref = $link.attr('href');
2549
2550 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2551 e.preventDefault();
2552 e.stopPropagation();
2553
2554 // Mark as tracked
2555 $link.attr('data-tracked', 'true');
2556
2557 // Get message context from the message div
2558 const messageText = messageDiv.text().substring(0, 200);
2559
2560 $.ajax({
2561 url: mxchatChat.ajax_url,
2562 type: 'POST',
2563 data: {
2564 action: 'mxchat_track_url_click',
2565 session_id: getChatSession(),
2566 url: originalHref,
2567 message_context: messageText,
2568 nonce: mxchatChat.nonce
2569 },
2570 complete: function() {
2571 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2572 window.open(originalHref, '_blank');
2573 } else {
2574 window.location.href = originalHref;
2575 }
2576 }
2577 });
2578
2579 return false;
2580 }
2581 }
2582 });
2583
2584
2585
2586
2587 // ====================================
2588 // MAIN INITIALIZATION
2589 // ====================================
2590
2591 if ($('#floating-chatbot').hasClass('hidden')) {
2592 $('#floating-chatbot-button').removeClass('hidden');
2593 }
2594 // Initialize when document is ready
2595 setFullHeight();
2596 initializeChatVisibility();
2597 loadChatHistory();
2598 trackOriginatingPage();
2599
2600 // Make functions globally available for add-ons
2601 window.hasQuickQuestions = hasQuickQuestions;
2602 window.collapseQuickQuestions = collapseQuickQuestions;
2603 window.appendMessage = appendMessage;
2604 window.appendThinkingMessage = appendThinkingMessage;
2605 window.scrollToBottom = scrollToBottom;
2606 window.scrollElementToTop = scrollElementToTop;
2607 window.replaceLastMessage = replaceLastMessage;
2608 window.callMxChat = callMxChat;
2609 window.callMxChatStream = callMxChatStream;
2610 window.shouldUseStreaming = shouldUseStreaming;
2611 window.getChatSession = getChatSession;
2612 window.getPageContext = getPageContext;
2613 window.updateStreamingMessage = updateStreamingMessage;
2614
2615 }); // End of jQuery ready
2616
2617
2618 // ====================================
2619 // GLOBAL EVENT LISTENERS (Outside jQuery)
2620 // ====================================
2621
2622 // Event listener for copy button (code blocks)
2623 document.addEventListener("click", (e) => {
2624 if (e.target.classList.contains("mxchat-copy-button")) {
2625 const copyButton = e.target;
2626 const codeBlock = copyButton
2627 .closest(".mxchat-code-block-container")
2628 .querySelector(".mxchat-code-block code");
2629
2630 if (codeBlock) {
2631 // Preserve formatting using innerText
2632 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
2633 copyButton.textContent = "Copied!";
2634 copyButton.setAttribute("aria-label", "Copied to clipboard");
2635
2636 setTimeout(() => {
2637 copyButton.textContent = "Copy";
2638 copyButton.setAttribute("aria-label", "Copy to clipboard");
2639 }, 2000);
2640 });
2641 }
2642 }
2643 });
2644
2645
2646
2647
2648
2649
2650