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

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

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