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

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