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

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

2,913 lines 112.1 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 // 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 // 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 // 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 // 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 // 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); // 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 // 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(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 isStreamingSupported(model) {
944 if (!model) return false;
945
946 const modelPrefix = model.split('-')[0].toLowerCase();
947
948 // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
949 const isSupported = modelPrefix === 'gpt' ||
950 modelPrefix === 'o1' ||
951 modelPrefix === 'claude' ||
952 modelPrefix === 'grok' ||
953 modelPrefix === 'deepseek' ||
954 model === 'openrouter'; // Add this line - check full model name for OpenRouter
955
956 return isSupported;
957 }
958
959 // Update the event handlers to use the correct function names
960 $('#send-button').off('click').on('click', function() {
961 sendMessage(); // Use the updated sendMessage function
962 });
963
964 // Override enter key handler
965 $('#chat-input').off('keypress').on('keypress', function(e) {
966 if (e.which == 13 && !e.shiftKey) {
967 e.preventDefault();
968 sendMessage(); // Use the updated sendMessage function
969 }
970 });
971
972
973 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
974 try {
975 // Determine styles based on sender type
976 let messageClass, bgColor, fontColor;
977
978 if (sender === "user") {
979 messageClass = "user-message";
980 bgColor = userMessageBgColor;
981 fontColor = userMessageFontColor;
982 // Only sanitize user input
983 messageText = sanitizeUserInput(messageText);
984 } else if (sender === "agent") {
985 messageClass = "agent-message";
986 bgColor = liveAgentMessageBgColor;
987 fontColor = liveAgentMessageFontColor;
988 } else {
989 messageClass = "bot-message";
990 bgColor = botMessageBgColor;
991 fontColor = botMessageFontColor;
992 }
993
994 const messageDiv = $('<div>')
995 .addClass(messageClass)
996 .attr('dir', 'auto')
997 .css({
998 'background': bgColor,
999 'color': fontColor,
1000 'margin-bottom': '1em'
1001 });
1002
1003 // Process the message content based on sender
1004 let fullMessage;
1005 if (sender === "user") {
1006 // For user messages, apply linkify after sanitization
1007 fullMessage = linkify(messageText);
1008 } else {
1009 // For bot/agent messages, preserve HTML
1010 fullMessage = messageText;
1011 }
1012
1013 // Add images if provided
1014 if (images && images.length > 0) {
1015 fullMessage += '<div class="image-gallery" dir="auto">';
1016 images.forEach(img => {
1017 const safeTitle = sanitizeUserInput(img.title);
1018 const safeUrl = encodeURI(img.image_url);
1019 const safeThumbnail = encodeURI(img.thumbnail_url);
1020
1021 fullMessage += `
1022 <div style="margin-bottom: 10px;">
1023 <strong>${safeTitle}</strong><br>
1024 <a href="${safeUrl}" target="_blank">
1025 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
1026 </a>
1027 </div>`;
1028 });
1029 fullMessage += '</div>';
1030 }
1031
1032 // Append HTML content if provided
1033 if (messageHtml && sender !== "user") {
1034 fullMessage += '<br><br>' + messageHtml;
1035 }
1036
1037 messageDiv.html(fullMessage);
1038
1039 if (isTemporary) {
1040 messageDiv.addClass('temporary-message');
1041 }
1042
1043 messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1044 // FIXED: Use event delegation for link tracking
1045 if (sender === "bot" || sender === "agent") {
1046 attachLinkTracking(messageDiv, messageText);
1047 }
1048
1049 if (sender === "bot") {
1050 const lastUserMessage = $('#chat-box').find('.user-message').last();
1051 if (lastUserMessage.length) {
1052 scrollElementToTop(lastUserMessage);
1053 }
1054 }
1055 });
1056
1057 if (messageText.id) {
1058 lastSeenMessageId = messageText.id;
1059 hideNotification();
1060 }
1061 } catch (error) {
1062 console.error("Error rendering message:", error);
1063 }
1064 }
1065
1066 // Helper function to attach link tracking with proper event handling
1067 function attachLinkTracking(messageDiv, messageText) {
1068 // Use a slight delay to ensure DOM is ready
1069 setTimeout(function() {
1070 const links = messageDiv.find('a[href]').not('[data-tracked]');
1071
1072 links.each(function() {
1073 const $link = $(this);
1074 const originalHref = $link.attr('href');
1075
1076 // Mark as tracked to avoid duplicate handlers
1077 $link.attr('data-tracked', 'true');
1078
1079 // Only track external URLs
1080 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1081 // Remove any existing click handlers first
1082 $link.off('click.tracking');
1083
1084 // Add new click handler with namespace
1085 $link.on('click.tracking', function(e) {
1086 e.preventDefault();
1087 e.stopPropagation();
1088
1089 const messageContext = typeof messageText === 'string'
1090 ? messageText.substring(0, 200)
1091 : '';
1092
1093 // Track the click
1094 $.ajax({
1095 url: mxchatChat.ajax_url,
1096 type: 'POST',
1097 data: {
1098 action: 'mxchat_track_url_click',
1099 session_id: getChatSession(),
1100 url: originalHref,
1101 message_context: messageContext,
1102 nonce: mxchatChat.nonce
1103 },
1104 complete: function() {
1105 // Always redirect, even if tracking fails
1106 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1107 window.open(originalHref, '_blank');
1108 } else {
1109 window.location.href = originalHref;
1110 }
1111 }
1112 });
1113
1114 return false; // Extra insurance to prevent default
1115 });
1116 }
1117 });
1118 }, 100); // Small delay to ensure DOM is ready
1119 }
1120
1121 function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1122 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1123 var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1124
1125 // Determine styles
1126 let bgColor, fontColor;
1127 if (sender === "user") {
1128 bgColor = userMessageBgColor;
1129 fontColor = userMessageFontColor;
1130 } else if (sender === "agent") {
1131 bgColor = liveAgentMessageBgColor;
1132 fontColor = liveAgentMessageFontColor;
1133 } else {
1134 bgColor = botMessageBgColor;
1135 fontColor = botMessageFontColor;
1136 }
1137
1138 var fullMessage = linkify(responseText);
1139 if (responseHtml) {
1140 fullMessage += '<br><br>' + responseHtml;
1141 }
1142
1143 if (images.length > 0) {
1144 fullMessage += '<div class="image-gallery" dir="auto">';
1145 images.forEach(img => {
1146 fullMessage += `
1147 <div style="margin-bottom: 10px;">
1148 <strong>${img.title}</strong><br>
1149 <a href="${img.image_url}" target="_blank">
1150 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1151 </a>
1152 </div>`;
1153 });
1154 fullMessage += '</div>';
1155 }
1156
1157 if (lastMessageDiv.length) {
1158 lastMessageDiv.fadeOut(200, function() {
1159 $(this)
1160 .html(fullMessage)
1161 .removeClass('bot-message user-message')
1162 .addClass(messageClass)
1163 .attr('dir', 'auto')
1164 .css({
1165 'background-color': bgColor,
1166 'color': fontColor,
1167 })
1168 .removeClass('temporary-message')
1169 .fadeIn(200, function() {
1170 // FIXED: Use the helper function for link tracking
1171 if (sender === "bot" || sender === "agent") {
1172 attachLinkTracking($(this), responseText);
1173 }
1174
1175 if (sender === "bot" || sender === "agent") {
1176 const lastUserMessage = $('#chat-box').find('.user-message').last();
1177 if (lastUserMessage.length) {
1178 scrollElementToTop(lastUserMessage);
1179 }
1180 // Show notification if chat is hidden
1181 if ($('#floating-chatbot').hasClass('hidden')) {
1182 showNotification();
1183 }
1184 }
1185 });
1186 });
1187 } else {
1188 appendMessage(sender, responseText, responseHtml, images);
1189 }
1190 }
1191
1192
1193 function appendThinkingMessage() {
1194 // Remove any existing thinking dots first
1195 $('.thinking-dots').remove();
1196
1197 // Retrieve the bot message font color and background color
1198 var botMessageFontColor = mxchatChat.bot_message_font_color;
1199 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1200
1201
1202 var thinkingHtml = '<div class="thinking-dots-container">' +
1203 '<div class="thinking-dots">' +
1204 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1205 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1206 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1207 '</div>' +
1208 '</div>';
1209
1210 // Append the thinking dots to the chat container (or within the temporary message div)
1211 $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1212 scrollToBottom();
1213 }
1214
1215 function removeThinkingDots() {
1216 $('.thinking-dots').closest('.temporary-message').remove();
1217 }
1218
1219 // ====================================
1220 // TEXT FORMATTING & PROCESSING
1221 // ====================================
1222
1223 function linkify(inputText) {
1224 //console.log('=== LINKIFY DEBUG START ===');
1225 //console.log('Input to linkify:', inputText);
1226 //console.log('Input type:', typeof inputText);
1227 //console.log('Input length:', inputText ? inputText.length : 'null/undefined');
1228
1229 if (!inputText) {
1230 //console.log('Empty input, returning empty string');
1231 return '';
1232 }
1233
1234 // Process markdown headers FIRST (before paragraph wrapping)
1235 //console.log('--- Processing Headers ---');
1236 let processedText = formatMarkdownHeaders(inputText);
1237 //console.log('After headers:', processedText);
1238 //console.log('Headers changed:', processedText !== inputText);
1239
1240 // Process text styling (bold, italic, strikethrough)
1241 //console.log('--- Processing Text Styling ---');
1242 //const beforeStyling = processedText;
1243 processedText = formatTextStyling(processedText);
1244 //console.log('After styling:', processedText);
1245 //console.log('Styling changed:', processedText !== beforeStyling);
1246
1247 // Process code blocks BEFORE processing links to avoid conflicts
1248 //console.log('--- Processing Code Blocks ---');
1249 const beforeCodeBlocks = processedText;
1250 processedText = formatCodeBlocks(processedText);
1251 //console.log('After code blocks:', processedText);
1252 //console.log('Code blocks changed:', processedText !== beforeCodeBlocks);
1253
1254 // NOW convert to paragraphs (this should be AFTER markdown processing)
1255 //console.log('--- Converting to Paragraphs ---');
1256 const beforeParagraphs = processedText;
1257 processedText = convertNewlinesToBreaks(processedText);
1258 //console.log('After paragraph conversion:', processedText);
1259 //console.log('Paragraphs changed:', processedText !== beforeParagraphs);
1260
1261 // Process markdown links
1262 //console.log('--- Processing Markdown Links ---');
1263 const beforeMarkdownLinks = processedText;
1264 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1265 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1266 //console.log('Found markdown link:', match, 'text:', text, 'url:', url);
1267 const safeUrl = encodeURI(url);
1268 const safeText = sanitizeUserInput(text);
1269 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1270 });
1271 //console.log('After markdown links:', processedText);
1272 //console.log('Markdown links changed:', processedText !== beforeMarkdownLinks);
1273
1274 // Process phone numbers (tel:)
1275 //console.log('--- Processing Phone Numbers ---');
1276 const beforePhone = processedText;
1277 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1278 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1279 //console.log('Found phone link:', match, 'text:', text, 'phone:', phone);
1280 const safePhone = encodeURI(phone);
1281 const safeText = sanitizeUserInput(text);
1282 return `<a href="${safePhone}">${safeText}</a>`;
1283 });
1284 //console.log('After phone numbers:', processedText);
1285 //console.log('Phone numbers changed:', processedText !== beforePhone);
1286
1287 // Process mailto links
1288 //console.log('--- Processing Mailto Links ---');
1289 const beforeMailto = processedText;
1290 const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1291 processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1292 console.log('Found mailto link:', match, 'text:', text, 'mailto:', mailto);
1293 const safeMailto = encodeURI(mailto);
1294 const safeText = sanitizeUserInput(text);
1295 return `<a href="${safeMailto}">${safeText}</a>`;
1296 });
1297 //console.log('After mailto links:', processedText);
1298 //console.log('Mailto links changed:', processedText !== beforeMailto);
1299
1300 // Process standalone URLs (avoid already processed links)
1301 //console.log('--- Processing Standalone URLs ---');
1302 const beforeStandaloneUrls = processedText;
1303 const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/gim;
1304 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1305 //console.log('Found standalone URL:', match, 'prefix:', prefix, 'url:', url);
1306 const safeUrl = encodeURI(url);
1307 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
1308 });
1309 //console.log('After standalone URLs:', processedText);
1310 //console.log('Standalone URLs changed:', processedText !== beforeStandaloneUrls);
1311
1312 // Process www. URLs (avoid already processed links)
1313 //console.log('--- Processing WWW URLs ---');
1314 const beforeWwwUrls = processedText;
1315 const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
1316 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1317 //console.log('Found www URL:', match, 'prefix:', prefix, 'url:', url);
1318 const safeUrl = encodeURI(`http://${url}`);
1319 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
1320 });
1321 //console.log('After www URLs:', processedText);
1322 //console.log('WWW URLs changed:', processedText !== beforeWwwUrls);
1323
1324 //console.log('=== FINAL RESULT ===');
1325 //console.log('Final processed text:', processedText);
1326 //console.log('Total transformation:', inputText !== processedText);
1327 //console.log('=== LINKIFY DEBUG END ===');
1328
1329 return processedText;
1330 }
1331
1332 function formatMarkdownHeaders(text) {
1333 // Handle h1 to h6 headers
1334 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1335 const level = hashes.length;
1336 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1337 });
1338 }
1339
1340 function formatTextStyling(text) {
1341 // Handle bold text (**text**)
1342 text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1343
1344 // Handle italic text (*text* or _text_) - avoid conflicts with bold
1345 text = text.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1346 text = text.replace(/(?<!_)_([^_\n]+)_(?!_)/g, '<em>$1</em>');
1347
1348 // Handle strikethrough (~~text~~)
1349 text = text.replace(/~~(.*?)~~/g, '<del>$1</del>');
1350
1351 return text;
1352 }
1353
1354 function formatBoldText(text) {
1355 // This function is kept for compatibility but now uses formatTextStyling
1356 return formatTextStyling(text);
1357 }
1358
1359 function convertNewlinesToBreaks(text) {
1360 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1361 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1362
1363 // Filter out empty paragraphs and wrap each paragraph in <p> tags
1364 return paragraphs
1365 .map(para => para.trim())
1366 .filter(para => para.length > 0) // Remove empty paragraphs
1367 .map(para => `<p>${para}</p>`)
1368 .join('');
1369 }
1370 function formatCodeBlocks(text) {
1371 // Handle fenced code blocks with language specification (```language)
1372 text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1373 const lang = language || 'text';
1374 const escapedCode = escapeHtml(code.trim());
1375 return `<div class="mxchat-code-block-container">
1376 <div class="mxchat-code-header">
1377 <span class="mxchat-code-language">${lang}</span>
1378 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1379 </div>
1380 <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1381 </div>`;
1382 });
1383
1384 // Handle inline code with single backticks
1385 text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1386
1387 // Handle raw PHP tags (legacy support)
1388 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1389 const escapedCode = escapeHtml(match);
1390 return `<div class="mxchat-code-block-container">
1391 <div class="mxchat-code-header">
1392 <span class="mxchat-code-language">php</span>
1393 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1394 </div>
1395 <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1396 </div>`;
1397 });
1398
1399 return text;
1400 }
1401
1402 function sanitizeUserInput(text) {
1403 const div = document.createElement('div');
1404 div.textContent = text;
1405 return div.innerHTML;
1406 }
1407
1408 function escapeHtml(unsafe) {
1409 // Skip escaping if it's already escaped or contains HTML code block markup
1410 if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
1411 unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1412 return unsafe;
1413 }
1414
1415 return unsafe
1416 .replace(/&/g, "&amp;")
1417 .replace(/</g, "&lt;")
1418 .replace(/>/g, "&gt;")
1419 .replace(/"/g, "&quot;")
1420 .replace(/'/g, "&#039;");
1421 }
1422
1423 function decodeHTMLEntities(text) {
1424 var textArea = document.createElement('textarea');
1425 textArea.innerHTML = text;
1426 return textArea.value;
1427 }
1428
1429 // ====================================
1430 // UI & SCROLLING CONTROLS
1431 // ====================================
1432
1433 function scrollToBottom(instant = false) {
1434 var chatBox = $('#chat-box');
1435 if (instant) {
1436 // Instantly set the scroll position to the bottom
1437 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1438 } else {
1439 // Use requestAnimationFrame for smoother scrolling if needed
1440 let start = null;
1441 const scrollHeight = chatBox.prop("scrollHeight");
1442 const initialScroll = chatBox.scrollTop();
1443 const distance = scrollHeight - initialScroll;
1444 const duration = 500; // Duration in ms
1445
1446 function smoothScroll(timestamp) {
1447 if (!start) start = timestamp;
1448 const progress = timestamp - start;
1449 const currentScroll = initialScroll + (distance * (progress / duration));
1450 chatBox.scrollTop(currentScroll);
1451
1452 if (progress < duration) {
1453 requestAnimationFrame(smoothScroll);
1454 } else {
1455 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1456 }
1457 }
1458
1459 requestAnimationFrame(smoothScroll);
1460 }
1461 }
1462
1463 function scrollElementToTop(element) {
1464 var chatBox = $('#chat-box');
1465 var elementTop = element.position().top + chatBox.scrollTop();
1466 chatBox.animate({ scrollTop: elementTop }, 500);
1467 }
1468
1469 function showChatWidget() {
1470 // First ensure display is set
1471 $('#floating-chatbot-button').css('display', 'flex');
1472 // Then handle the fade
1473 $('#floating-chatbot-button').fadeTo(500, 1);
1474 // Force visibility
1475 $('#floating-chatbot-button').removeClass('hidden');
1476 //console.log('Showing widget');
1477 }
1478
1479 function hideChatWidget() {
1480 $('#floating-chatbot-button').css('display', 'none');
1481 $('#floating-chatbot-button').addClass('hidden');
1482 //console.log('Hiding widget');
1483 }
1484
1485 function disableScroll() {
1486 if (isMobile()) {
1487 $('body').css('overflow', 'hidden');
1488 }
1489 }
1490
1491 function enableScroll() {
1492 if (isMobile()) {
1493 $('body').css('overflow', '');
1494 }
1495 }
1496
1497 function isMobile() {
1498 // This can be a simple check, or more sophisticated detection of mobile devices
1499 return window.innerWidth <= 768; // Example threshold for mobile devices
1500 }
1501
1502 function setFullHeight() {
1503 var vh = $(window).innerHeight() * 0.01;
1504 $(':root').css('--vh', vh + 'px');
1505 }
1506
1507
1508 // ====================================
1509 // NOTIFICATION SYSTEM
1510 // ====================================
1511
1512 function createNotificationBadge() {
1513 //console.log("Creating notification badge...");
1514 const chatButton = document.getElementById('floating-chatbot-button');
1515 //console.log("Chat button found:", !!chatButton);
1516
1517 if (!chatButton) return;
1518
1519 // Remove any existing badge first
1520 const existingBadge = chatButton.querySelector('.chat-notification-badge');
1521 if (existingBadge) {
1522 //console.log("Removing existing badge");
1523 existingBadge.remove();
1524 }
1525
1526 notificationBadge = document.createElement('div');
1527 notificationBadge.className = 'chat-notification-badge';
1528 notificationBadge.style.cssText = `
1529 display: none;
1530 position: absolute;
1531 top: -5px;
1532 right: -5px;
1533 background-color: red;
1534 color: white;
1535 border-radius: 50%;
1536 padding: 4px 8px;
1537 font-size: 12px;
1538 font-weight: bold;
1539 z-index: 10001;
1540 `;
1541 chatButton.style.position = 'relative';
1542 chatButton.appendChild(notificationBadge);
1543
1544 }
1545
1546 function showNotification() {
1547 const badge = document.getElementById('chat-notification-badge');
1548 if (badge && $('#floating-chatbot').hasClass('hidden')) {
1549 badge.style.display = 'block';
1550 badge.textContent = '1';
1551 }
1552 }
1553
1554 function hideNotification() {
1555 const badge = document.getElementById('chat-notification-badge');
1556 if (badge) {
1557 badge.style.display = 'none';
1558 }
1559 }
1560
1561 function startNotificationChecking() {
1562 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1563 if (!chatPersistenceEnabled) return;
1564
1565 createNotificationBadge();
1566 notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
1567 }
1568
1569 function stopNotificationChecking() {
1570 if (notificationCheckInterval) {
1571 clearInterval(notificationCheckInterval);
1572 }
1573 }
1574
1575 function checkForNewMessages() {
1576 const sessionId = getChatSession();
1577 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1578
1579 if (!chatPersistenceEnabled) return;
1580
1581 $.ajax({
1582 url: mxchatChat.ajax_url,
1583 type: 'POST',
1584 data: {
1585 action: 'mxchat_check_new_messages',
1586 session_id: sessionId,
1587 last_seen_id: lastSeenMessageId,
1588 nonce: mxchatChat.nonce
1589 },
1590 success: function(response) {
1591 if (response.success && response.data.hasNewMessages) {
1592 showNotification();
1593 }
1594 }
1595 });
1596 }
1597
1598
1599 // ====================================
1600 // LIVE AGENT FUNCTIONALITY
1601 // ====================================
1602
1603 function updateChatModeIndicator(mode) {
1604 //console.log("updateChatModeIndicator called with mode:", mode);
1605 const indicator = document.getElementById('chat-mode-indicator');
1606 if (indicator) {
1607 const oldText = indicator.textContent;
1608
1609 if (mode === 'agent') {
1610 indicator.textContent = 'Live Agent';
1611 startPolling();
1612 //console.log("Set indicator to Live Agent");
1613 } else {
1614 // Everything else is AI mode
1615 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1616 indicator.textContent = customAiText;
1617 stopPolling();
1618 //console.log("Set indicator to AI Agent:", customAiText);
1619 }
1620
1621 // Force immediate DOM update
1622 if (oldText !== indicator.textContent) {
1623 // Force a reflow to ensure the change is visible immediately
1624 indicator.offsetHeight;
1625
1626 // Double-check after a brief moment
1627 setTimeout(() => {
1628 //console.log("Final indicator text:", indicator.textContent);
1629 }, 50);
1630 }
1631 } else {
1632 console.error("Chat mode indicator element not found!");
1633 }
1634 }
1635
1636 function startPolling() {
1637 // Clear any existing interval first
1638 stopPolling();
1639 // Start new polling interval
1640 pollingInterval = setInterval(checkForAgentMessages, 5000);
1641 //console.log("Started agent message polling");
1642 }
1643
1644 function stopPolling() {
1645 if (pollingInterval) {
1646 clearInterval(pollingInterval);
1647 pollingInterval = null;
1648 //console.log("Stopped agent message polling");
1649 }
1650 }
1651
1652 function checkForAgentMessages() {
1653 const sessionId = getChatSession();
1654 $.ajax({
1655 url: mxchatChat.ajax_url,
1656 type: 'POST',
1657 dataType: 'json',
1658 data: {
1659 action: 'mxchat_fetch_new_messages',
1660 session_id: sessionId,
1661 last_seen_id: lastSeenMessageId,
1662 persistence_enabled: 'true', // Add this too
1663 nonce: mxchatChat.nonce
1664 },
1665 success: function (response) {
1666 if (response.success && response.data?.new_messages) {
1667 let hasNewMessage = false;
1668
1669 response.data.new_messages.forEach(function (message) {
1670 if (message.role === "agent" && !processedMessageIds.has(message.id)) {
1671 hasNewMessage = true;
1672 // CHANGE THIS LINE:
1673 appendMessage("agent", message.content); // Instead of replaceLastMessage
1674 lastSeenMessageId = message.id;
1675 processedMessageIds.add(message.id);
1676 }
1677 });
1678
1679 if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1680 showNotification();
1681 }
1682
1683 scrollToBottom(true);
1684 }
1685 },
1686 error: function (xhr, status, error) {
1687 //console.error("Polling error:", xhr, status, error);
1688 }
1689 });
1690 }
1691
1692 // ====================================
1693 // CHAT HISTORY & PERSISTENCE
1694 // ====================================
1695
1696 function loadChatHistory() {
1697 var sessionId = getChatSession();
1698 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1699
1700 if (chatPersistenceEnabled && sessionId) {
1701 $.ajax({
1702 url: mxchatChat.ajax_url,
1703 type: 'POST',
1704 dataType: 'json',
1705 data: {
1706 action: 'mxchat_fetch_conversation_history',
1707 session_id: sessionId
1708 },
1709 success: function(response) {
1710 // Check if the response indicates success
1711 if (response.success) {
1712 // Handle case where conversation data exists and is an array
1713 if (response.data && Array.isArray(response.data.conversation)) {
1714 var $chatBox = $('#chat-box');
1715 var $fragment = $(document.createDocumentFragment());
1716 let highestMessageId = lastSeenMessageId;
1717
1718 // Update chat mode if provided
1719 if (response.data.chat_mode) {
1720 updateChatModeIndicator(response.data.chat_mode);
1721 }
1722
1723 // Only process if there are actual messages
1724 if (response.data.conversation.length > 0) {
1725 $.each(response.data.conversation, function(index, message) {
1726 // Skip agent messages if persistence is off
1727 if (!chatPersistenceEnabled && message.role === 'agent') {
1728 return;
1729 }
1730
1731 var messageClass, messageBgColor, messageFontColor;
1732
1733 switch (message.role) {
1734 case 'user':
1735 messageClass = 'user-message';
1736 messageBgColor = userMessageBgColor;
1737 messageFontColor = userMessageFontColor;
1738 break;
1739 case 'agent':
1740 messageClass = 'agent-message';
1741 messageBgColor = liveAgentMessageBgColor;
1742 messageFontColor = liveAgentMessageFontColor;
1743 break;
1744 default:
1745 messageClass = 'bot-message';
1746 messageBgColor = botMessageBgColor;
1747 messageFontColor = botMessageFontColor;
1748 break;
1749 }
1750
1751 var messageElement = $('<div>').addClass(messageClass)
1752 .css({
1753 'background': messageBgColor,
1754 'color': messageFontColor
1755 });
1756
1757 var content = message.content;
1758 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1759 content = decodeHTMLEntities(content);
1760
1761 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1762 messageElement.html(content);
1763 } else {
1764 var formattedContent = linkify(content);
1765 messageElement.html(formattedContent);
1766 }
1767
1768 $fragment.append(messageElement);
1769
1770 // Track message IDs
1771 if (message.id) {
1772 highestMessageId = Math.max(highestMessageId, message.id);
1773 processedMessageIds.add(message.id);
1774 }
1775 });
1776
1777 // Only append messages and scroll if we have content
1778 $chatBox.append($fragment);
1779 scrollToBottom(true);
1780
1781 // Collapse quick questions if we have conversation history
1782 if (hasQuickQuestions()) {
1783 collapseQuickQuestions();
1784 }
1785
1786 // Update lastSeenMessageId after history loads
1787 lastSeenMessageId = highestMessageId;
1788
1789 // Only update chat mode if persistence is enabled and we have messages
1790 if (chatPersistenceEnabled) {
1791 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1792 if (lastMessage.role === 'agent') {
1793 updateChatModeIndicator('agent');
1794 }
1795 }
1796 } else {
1797 // No conversation history - this is normal for new chats
1798 //console.log("No conversation history found for this session - starting fresh chat.");
1799 }
1800 } else {
1801 // Response successful but no conversation data - this is also normal
1802 //console.log("No conversation data in response - starting fresh chat.");
1803 }
1804 } else {
1805 // Only show error if the response explicitly indicates an error
1806 console.warn("Failed to load chat history:", response.message || "Unknown error");
1807 // Don't show error message to user for failed history loads
1808 // Just start with a fresh chat instead
1809 }
1810 },
1811 error: function(xhr, status, error) {
1812 // Only log AJAX errors, don't show them to the user
1813 console.error("AJAX error loading chat history:", status, error);
1814 // Start with fresh chat - don't show error to user
1815 }
1816 });
1817 } else {
1818 //console.log("Chat persistence is disabled or no session ID found. Starting fresh chat.");
1819 }
1820 }
1821
1822
1823 // ====================================
1824 // FILE UPLOAD FUNCTIONALITY
1825 // ====================================
1826
1827 function addSafeEventListener(elementId, eventType, handler) {
1828 const element = document.getElementById(elementId);
1829 if (element) {
1830 element.addEventListener(eventType, handler);
1831 }
1832 }
1833
1834 function showActivePdf(filename) {
1835 const container = document.getElementById('active-pdf-container');
1836 const nameElement = document.getElementById('active-pdf-name');
1837
1838 if (!container || !nameElement) {
1839 //console.error('PDF container elements not found');
1840 return;
1841 }
1842
1843 nameElement.textContent = filename;
1844 container.style.display = 'flex';
1845 }
1846
1847 function showActiveWord(filename) {
1848 const container = document.getElementById('active-word-container');
1849 const nameElement = document.getElementById('active-word-name');
1850
1851 if (!container || !nameElement) {
1852 //console.error('Word document container elements not found');
1853 return;
1854 }
1855
1856 nameElement.textContent = filename;
1857 container.style.display = 'flex';
1858 }
1859
1860 function removeActivePdf() {
1861 const container = document.getElementById('active-pdf-container');
1862 const nameElement = document.getElementById('active-pdf-name');
1863
1864 if (!container || !nameElement || !activePdfFile) return;
1865
1866 fetch(mxchatChat.ajax_url, {
1867 method: 'POST',
1868 headers: {
1869 'Content-Type': 'application/x-www-form-urlencoded',
1870 },
1871 body: new URLSearchParams({
1872 'action': 'mxchat_remove_pdf',
1873 'session_id': sessionId,
1874 'nonce': mxchatChat.nonce
1875 })
1876 })
1877 .then(response => response.json())
1878 .then(data => {
1879 if (data.success) {
1880 container.style.display = 'none';
1881 nameElement.textContent = '';
1882 activePdfFile = null;
1883 appendMessage('bot', 'PDF removed.');
1884 }
1885 })
1886 .catch(error => {
1887 //console.error('Error removing PDF:', error);
1888 });
1889 }
1890
1891 function removeActiveWord() {
1892 const container = document.getElementById('active-word-container');
1893 const nameElement = document.getElementById('active-word-name');
1894
1895 if (!container || !nameElement || !activeWordFile) return;
1896
1897 fetch(mxchatChat.ajax_url, {
1898 method: 'POST',
1899 headers: {
1900 'Content-Type': 'application/x-www-form-urlencoded',
1901 },
1902 body: new URLSearchParams({
1903 'action': 'mxchat_remove_word',
1904 'session_id': sessionId,
1905 'nonce': mxchatChat.nonce
1906 })
1907 })
1908 .then(response => response.json())
1909 .then(data => {
1910 if (data.success) {
1911 container.style.display = 'none';
1912 nameElement.textContent = '';
1913 activeWordFile = null;
1914 appendMessage('bot', 'Word document removed.');
1915 }
1916 })
1917 .catch(error => {
1918 //console.error('Error removing Word document:', error);
1919 });
1920 }
1921
1922 // ====================================
1923 // CONSENT & COMPLIANCE (GDPR)
1924 // ====================================
1925
1926 function initializeChatVisibility() {
1927 //console.log('Initializing chat visibility');
1928 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1929 mxchatChat.complianz_toggle === '1' ||
1930 mxchatChat.complianz_toggle === 1;
1931
1932 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1933 // Initial check
1934 checkConsentAndShowChat();
1935
1936 // Listen for consent changes
1937 $(document).on('cmplz_status_change', function(event) {
1938 //console.log('Status change detected');
1939 checkConsentAndShowChat();
1940 });
1941 } else {
1942 // If Complianz is not enabled, always show
1943 $('#floating-chatbot-button')
1944 .css('display', 'flex')
1945 .removeClass('hidden no-consent')
1946 .fadeTo(500, 1);
1947
1948 // Also check pre-chat message when Complianz is not enabled
1949 checkPreChatDismissal();
1950 }
1951 }
1952
1953
1954 function checkConsentAndShowChat() {
1955 var consentStatus = cmplz_has_consent('marketing');
1956 var consentType = complianz.consenttype;
1957
1958 //console.log('Checking consent:', {status: consentStatus,type: consentType});
1959
1960 let $widget = $('#floating-chatbot-button');
1961 let $chatbot = $('#floating-chatbot');
1962 let $preChat = $('#pre-chat-message');
1963
1964 if (consentStatus === true) {
1965 //console.log('Consent granted - showing widget');
1966 $widget
1967 .removeClass('no-consent')
1968 .css('display', 'flex')
1969 .removeClass('hidden')
1970 .fadeTo(500, 1);
1971 $chatbot.removeClass('no-consent');
1972
1973 // Show pre-chat message if not dismissed
1974 checkPreChatDismissal();
1975 } else {
1976 //console.log('No consent - hiding widget');
1977 $widget
1978 .addClass('no-consent')
1979 .fadeTo(500, 0, function() {
1980 $(this)
1981 .css('display', 'none')
1982 .addClass('hidden');
1983 });
1984 $chatbot.addClass('no-consent');
1985
1986 // Hide pre-chat message when no consent
1987 $preChat.hide();
1988 }
1989 }
1990
1991
1992 // ====================================
1993 // PRE-CHAT MESSAGE HANDLING
1994 // ====================================
1995
1996 function checkPreChatDismissal() {
1997 $.ajax({
1998 url: mxchatChat.ajax_url,
1999 type: 'POST',
2000 data: {
2001 action: 'mxchat_check_pre_chat_message_status',
2002 _ajax_nonce: mxchatChat.nonce
2003 },
2004 success: function(response) {
2005 if (response.success && !response.data.dismissed) {
2006 $('#pre-chat-message').fadeIn(250);
2007 } else {
2008 $('#pre-chat-message').hide();
2009 }
2010 },
2011 error: function() {
2012 //console.error('Failed to check pre-chat message dismissal status.');
2013 }
2014 });
2015 }
2016
2017 function handlePreChatDismissal() {
2018 $('#pre-chat-message').fadeOut(200);
2019 $.ajax({
2020 url: mxchatChat.ajax_url,
2021 type: 'POST',
2022 data: {
2023 action: 'mxchat_dismiss_pre_chat_message',
2024 _ajax_nonce: mxchatChat.nonce
2025 },
2026 success: function() {
2027 $('#pre-chat-message').hide();
2028 },
2029 error: function() {
2030 //console.error('Failed to dismiss pre-chat message.');
2031 }
2032 });
2033 }
2034
2035
2036 // ====================================
2037 // UTILITY FUNCTIONS
2038 // ====================================
2039
2040 function copyToClipboard(text) {
2041 var tempInput = $('<input>');
2042 $('body').append(tempInput);
2043 tempInput.val(text).select();
2044 document.execCommand('copy');
2045 tempInput.remove();
2046 }
2047
2048
2049 function isImageHtml(str) {
2050 return str.startsWith('<img') && str.endsWith('>');
2051 }
2052
2053
2054 // ====================================
2055 // EVENT HANDLERS & INITIALIZATION
2056 // ====================================
2057
2058 $(document).on('click', '.mxchat-popular-question', function () {
2059 var question = $(this).text();
2060
2061 // Append the question as if the user typed it
2062 appendMessage("user", question);
2063
2064 // Only collapse if there are questions
2065 if (hasQuickQuestions()) {
2066 collapseQuickQuestions();
2067 }
2068
2069 // Send the question to the server
2070 sendMessageToChatbot(question);
2071 });
2072
2073 $(document).on('click', '.questions-toggle-btn', function(e) {
2074 e.preventDefault();
2075 e.stopPropagation();
2076 expandQuickQuestions();
2077 });
2078
2079 $(document).on('click', '.questions-collapse-btn', function(e) {
2080 e.preventDefault();
2081 e.stopPropagation();
2082 collapseQuickQuestions();
2083 });
2084
2085 // Chatbot visibility toggle handlers
2086 $(document).on('click', '#floating-chatbot-button', function() {
2087 var chatbot = $('#floating-chatbot');
2088 if (chatbot.hasClass('hidden')) {
2089 chatbot.removeClass('hidden').addClass('visible');
2090 $(this).addClass('hidden');
2091 $('#chat-notification-badge').hide(); // Hide notification when opening chat
2092 disableScroll();
2093 $('#pre-chat-message').fadeOut(250);
2094 } else {
2095 chatbot.removeClass('visible').addClass('hidden');
2096 $(this).removeClass('hidden');
2097 enableScroll();
2098 checkPreChatDismissal();
2099 }
2100 });
2101
2102 $(document).on('click', '#exit-chat-button', function() {
2103 $('#floating-chatbot').addClass('hidden').removeClass('visible');
2104 $('#floating-chatbot-button').removeClass('hidden');
2105 enableScroll();
2106 });
2107
2108 $(document).on('click', '.close-pre-chat-message', function(e) {
2109 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2110 $('#pre-chat-message').fadeOut(200, function() {
2111 $(this).remove();
2112 });
2113 });
2114
2115
2116 // PDF upload button handlers
2117 if (document.getElementById('pdf-upload-btn')) {
2118 document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2119 document.getElementById('pdf-upload').click();
2120 });
2121 }
2122
2123 // Word upload button handlers
2124 if (document.getElementById('word-upload-btn')) {
2125 document.getElementById('word-upload-btn').addEventListener('click', function() {
2126 document.getElementById('word-upload').click();
2127 });
2128 }
2129
2130 // PDF file input change handler
2131 addSafeEventListener('pdf-upload', 'change', async function(e) {
2132 const file = e.target.files[0];
2133
2134 if (!file || file.type !== 'application/pdf') {
2135 alert('Please select a valid PDF file.');
2136 return;
2137 }
2138
2139 if (!sessionId) {
2140 //console.error('No session ID found');
2141 alert('Error: No session ID found');
2142 return;
2143 }
2144
2145 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2146 //console.error('mxchatChat not properly configured:', mxchatChat);
2147 alert('Error: Ajax configuration missing');
2148 return;
2149 }
2150
2151 // Disable buttons and show loading state
2152 const uploadBtn = document.getElementById('pdf-upload-btn');
2153 const sendBtn = document.getElementById('send-button');
2154 const originalBtnContent = uploadBtn.innerHTML;
2155
2156 try {
2157 const formData = new FormData();
2158 formData.append('action', 'mxchat_upload_pdf');
2159 formData.append('pdf_file', file);
2160 formData.append('session_id', sessionId);
2161 formData.append('nonce', mxchatChat.nonce);
2162
2163 uploadBtn.disabled = true;
2164 sendBtn.disabled = true;
2165 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2166 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2167 </svg>`;
2168
2169 const response = await fetch(mxchatChat.ajax_url, {
2170 method: 'POST',
2171 body: formData
2172 });
2173
2174 const data = await response.json();
2175
2176 if (data.success) {
2177 // Hide popular questions if they exist
2178 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2179 if (hasQuickQuestions()) {
2180 collapseQuickQuestions();
2181 }
2182
2183 // Show the active PDF name
2184 showActivePdf(data.data.filename);
2185
2186 appendMessage('bot', data.data.message);
2187 scrollToBottom();
2188 activePdfFile = data.data.filename;
2189 } else {
2190 //console.error('Upload failed:', data.data);
2191 alert('Failed to upload PDF. Please try again.');
2192 }
2193 } catch (error) {
2194 //console.error('Upload error:', error);
2195 alert('Error uploading file. Please try again.');
2196 } finally {
2197 uploadBtn.disabled = false;
2198 sendBtn.disabled = false;
2199 uploadBtn.innerHTML = originalBtnContent;
2200 this.value = ''; // Reset file input
2201 }
2202 });
2203
2204 // Word file input change handler
2205 addSafeEventListener('word-upload', 'change', async function(e) {
2206 const file = e.target.files[0];
2207
2208 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2209 alert('Please select a valid Word document (.docx).');
2210 return;
2211 }
2212
2213 if (!sessionId) {
2214 //console.error('No session ID found');
2215 alert('Error: No session ID found');
2216 return;
2217 }
2218
2219 // Disable buttons and show loading state
2220 const uploadBtn = document.getElementById('word-upload-btn');
2221 const sendBtn = document.getElementById('send-button');
2222 const originalBtnContent = uploadBtn.innerHTML;
2223
2224 try {
2225 const formData = new FormData();
2226 formData.append('action', 'mxchat_upload_word');
2227 formData.append('word_file', file);
2228 formData.append('session_id', sessionId);
2229 formData.append('nonce', mxchatChat.nonce);
2230
2231 uploadBtn.disabled = true;
2232 sendBtn.disabled = true;
2233 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2234 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2235 </svg>`;
2236
2237 const response = await fetch(mxchatChat.ajax_url, {
2238 method: 'POST',
2239 body: formData
2240 });
2241
2242 const data = await response.json();
2243
2244 if (data.success) {
2245 // Hide popular questions if they exist
2246 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2247 if (hasQuickQuestions()) {
2248 collapseQuickQuestions();
2249 }
2250
2251 // Show the active Word document name
2252 showActiveWord(data.data.filename);
2253
2254 appendMessage('bot', data.data.message);
2255 scrollToBottom();
2256 activeWordFile = data.data.filename;
2257 } else {
2258 //console.error('Upload failed:', data.data);
2259 alert('Failed to upload Word document. Please try again.');
2260 }
2261 } catch (error) {
2262 //console.error('Upload error:', error);
2263 alert('Error uploading file. Please try again.');
2264 } finally {
2265 uploadBtn.disabled = false;
2266 sendBtn.disabled = false;
2267 uploadBtn.innerHTML = originalBtnContent;
2268 this.value = ''; // Reset file input
2269 }
2270 });
2271
2272 // Remove button click handlers
2273 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2274 e.preventDefault();
2275 e.stopPropagation();
2276 removeActivePdf();
2277 });
2278
2279 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2280 e.preventDefault();
2281 e.stopPropagation();
2282 removeActiveWord();
2283 });
2284
2285 // Window resize handlers
2286 $(window).on('resize orientationchange', function() {
2287 setFullHeight();
2288 });
2289
2290
2291 // ====================================
2292 // TOOLBAR & STYLING SETUP
2293 // ====================================
2294
2295 // Apply toolbar settings
2296 if (mxchatChat.chat_toolbar_toggle === 'on') {
2297 $('.chat-toolbar').show();
2298 } else {
2299 $('.chat-toolbar').hide();
2300 }
2301
2302 // Apply toolbar icon colors
2303 const toolbarElements = [
2304 '#mxchat-chatbot .toolbar-btn svg',
2305 '#mxchat-chatbot .active-pdf-name',
2306 '#mxchat-chatbot .active-word-name',
2307 '#mxchat-chatbot .remove-pdf-btn svg',
2308 '#mxchat-chatbot .remove-word-btn svg',
2309 '#mxchat-chatbot .toolbar-perplexity svg'
2310 ];
2311
2312 toolbarElements.forEach(selector => {
2313 $(selector).css({
2314 'fill': toolbarIconColor,
2315 'stroke': toolbarIconColor,
2316 'color': toolbarIconColor
2317 });
2318 });
2319
2320
2321 // ====================================
2322 // EMAIL COLLECTION SETUP - FIXED VERSION
2323 // ====================================
2324 // Only run email collection setup if it's enabled
2325 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2326 //console.log('Email collection is enabled, setting up handlers...');
2327
2328 // Email collection form setup and handlers
2329 const emailForm = document.getElementById('email-collection-form');
2330 const emailBlocker = document.getElementById('email-blocker');
2331 const chatbotWrapper = document.getElementById('chat-container');
2332
2333 if (emailForm && emailBlocker && chatbotWrapper) {
2334
2335 // Add loading state management
2336 let isSubmitting = false;
2337
2338 // Optimized UI transition functions
2339 function showEmailForm() {
2340 emailBlocker.style.display = 'flex';
2341 chatbotWrapper.style.display = 'none';
2342 }
2343
2344 function showChatContainer() {
2345 // Show chat immediately without delay
2346 emailBlocker.style.display = 'none';
2347 chatbotWrapper.style.display = 'flex';
2348
2349 // Load chat history only after showing chat container
2350 if (typeof loadChatHistory === 'function') {
2351 loadChatHistory();
2352 }
2353 }
2354
2355 // Enhanced email validation
2356 function isValidEmail(email) {
2357 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2358 return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2359 }
2360
2361 // Enhanced name validation
2362 function isValidName(name) {
2363 return name && name.trim().length >= 2 && name.trim().length <= 100;
2364 }
2365
2366 // Show loading state with spinner
2367 function setSubmissionState(loading) {
2368 const submitButton = document.getElementById('email-submit-button');
2369 const emailInput = document.getElementById('user-email');
2370 const nameInput = document.getElementById('user-name');
2371
2372 if (loading) {
2373 isSubmitting = true;
2374 if (submitButton) submitButton.disabled = true;
2375 if (emailInput) emailInput.disabled = true;
2376 if (nameInput) nameInput.disabled = true;
2377
2378 // Store original content and add spinner
2379 if (submitButton && !submitButton.getAttribute('data-original-html')) {
2380 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2381
2382 // Add loading spinner while keeping original text
2383 const originalText = submitButton.textContent;
2384 submitButton.innerHTML = `
2385 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2386 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2387 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2388 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2389 </circle>
2390 </svg>
2391 ${originalText}
2392 `;
2393
2394 submitButton.style.opacity = '0.8';
2395 }
2396 } else {
2397 isSubmitting = false;
2398 if (submitButton) submitButton.disabled = false;
2399 if (emailInput) emailInput.disabled = false;
2400 if (nameInput) nameInput.disabled = false;
2401
2402 // Restore original content
2403 if (submitButton) {
2404 const originalHtml = submitButton.getAttribute('data-original-html');
2405 if (originalHtml) {
2406 submitButton.innerHTML = originalHtml;
2407 }
2408 submitButton.style.opacity = '1';
2409 }
2410 }
2411 }
2412
2413 // Error display functions
2414 function showEmailError(message) {
2415 clearEmailError();
2416
2417 const errorDiv = document.createElement('div');
2418 errorDiv.className = 'email-error';
2419 errorDiv.style.cssText = `
2420 color: #e74c3c;
2421 font-size: 12px;
2422 margin-top: 8px;
2423 padding: 4px 0;
2424 animation: fadeInError 0.3s ease;
2425 `;
2426 errorDiv.textContent = message;
2427
2428 // Add CSS animation if not already present
2429 if (!document.getElementById('email-error-styles')) {
2430 const style = document.createElement('style');
2431 style.id = 'email-error-styles';
2432 style.textContent = `
2433 @keyframes fadeInError {
2434 from { opacity: 0; transform: translateY(-5px); }
2435 to { opacity: 1; transform: translateY(0); }
2436 }
2437 .email-input-shake {
2438 animation: shake 0.5s ease-in-out;
2439 }
2440 @keyframes shake {
2441 0%, 100% { transform: translateX(0); }
2442 25% { transform: translateX(-5px); }
2443 75% { transform: translateX(5px); }
2444 }
2445 @keyframes spin {
2446 from { transform: rotate(0deg); }
2447 to { transform: rotate(360deg); }
2448 }
2449 .email-spinner {
2450 display: inline-block;
2451 vertical-align: middle;
2452 }
2453 `;
2454 document.head.appendChild(style);
2455 }
2456
2457 emailForm.appendChild(errorDiv);
2458
2459 // Add shake animation to inputs
2460 const emailInput = document.getElementById('user-email');
2461 const nameInput = document.getElementById('user-name');
2462
2463 if (emailInput) {
2464 emailInput.classList.add('email-input-shake');
2465 setTimeout(() => {
2466 emailInput.classList.remove('email-input-shake');
2467 }, 500);
2468 }
2469
2470 if (nameInput) {
2471 nameInput.classList.add('email-input-shake');
2472 setTimeout(() => {
2473 nameInput.classList.remove('email-input-shake');
2474 }, 500);
2475 }
2476 }
2477
2478 function clearEmailError() {
2479 const existingErrors = emailForm.querySelectorAll('.email-error');
2480 existingErrors.forEach(error => error.remove());
2481 }
2482
2483 // MAIN FORM SUBMIT HANDLER - This is the critical fix
2484 //console.log('Setting up form submit handler...');
2485
2486 // Remove any existing event listeners first
2487 emailForm.removeEventListener('submit', handleFormSubmit);
2488
2489 // Add the form submit handler
2490 emailForm.addEventListener('submit', handleFormSubmit);
2491
2492 function handleFormSubmit(event) {
2493 //console.log('Form submit handler triggered!');
2494 event.preventDefault();
2495 event.stopPropagation();
2496
2497 // Prevent double submission
2498 if (isSubmitting) {
2499 //console.log('Already submitting, ignoring...');
2500 return false;
2501 }
2502
2503 const userEmail = document.getElementById('user-email').value.trim();
2504 const nameInput = document.getElementById('user-name');
2505 const userName = nameInput ? nameInput.value.trim() : '';
2506 const sessionId = getChatSession();
2507
2508 //console.log('Form data:', { userEmail, userName, sessionId });
2509
2510 // Validate email before submission
2511 if (!userEmail) {
2512 showEmailError('Please enter your email address.');
2513 return false;
2514 }
2515
2516 if (!isValidEmail(userEmail)) {
2517 showEmailError('Please enter a valid email address.');
2518 return false;
2519 }
2520
2521 // Validate name if field exists
2522 if (nameInput && !isValidName(userName)) {
2523 showEmailError('Please enter a valid name (2-100 characters).');
2524 return false;
2525 }
2526
2527 // Clear any existing errors
2528 clearEmailError();
2529 setSubmissionState(true);
2530
2531 //console.log('Sending AJAX request...');
2532
2533 // Prepare form data with optional name
2534 const formData = new URLSearchParams({
2535 action: 'mxchat_handle_save_email_and_response',
2536 email: userEmail,
2537 session_id: sessionId,
2538 nonce: mxchatChat.nonce,
2539 });
2540
2541 // Add name to form data if provided
2542 if (userName) {
2543 formData.append('name', userName);
2544 }
2545
2546 fetch(mxchatChat.ajax_url, {
2547 method: 'POST',
2548 headers: {
2549 'Content-Type': 'application/x-www-form-urlencoded',
2550 },
2551 body: formData
2552 })
2553 .then((response) => {
2554 //console.log('Response received:', response);
2555 if (!response.ok) {
2556 throw new Error(`HTTP error! status: ${response.status}`);
2557 }
2558 return response.json();
2559 })
2560 .then((data) => {
2561 //console.log('Response data:', data);
2562 setSubmissionState(false);
2563
2564 if (data.success) {
2565 // Show chat immediately
2566 showChatContainer();
2567
2568 // Handle bot response if provided
2569 if (data.message && typeof appendMessage === 'function') {
2570 setTimeout(() => {
2571 appendMessage('bot', data.message);
2572 if (typeof scrollToBottom === 'function') {
2573 scrollToBottom();
2574 }
2575 }, 100);
2576 }
2577 } else {
2578 showEmailError(data.message || 'Failed to save email. Please try again.');
2579 }
2580 })
2581 .catch((error) => {
2582 console.error('Email submission error:', error);
2583 setSubmissionState(false);
2584 showEmailError('An error occurred. Please try again.');
2585 });
2586
2587 return false; // Extra prevention
2588 }
2589
2590 // Real-time email validation
2591 const emailInput = document.getElementById('user-email');
2592 if (emailInput) {
2593 let validationTimeout;
2594
2595 emailInput.addEventListener('input', function() {
2596 // Clear previous validation timeout
2597 if (validationTimeout) {
2598 clearTimeout(validationTimeout);
2599 }
2600
2601 // Debounce validation
2602 validationTimeout = setTimeout(() => {
2603 const email = this.value.trim();
2604 clearEmailError();
2605
2606 if (email && !isValidEmail(email)) {
2607 showEmailError('Please enter a valid email address.');
2608 }
2609 }, 500);
2610 });
2611
2612 // Handle Enter key
2613 emailInput.addEventListener('keypress', function(e) {
2614 if (e.key === 'Enter' && !isSubmitting) {
2615 e.preventDefault();
2616 emailForm.dispatchEvent(new Event('submit'));
2617 }
2618 });
2619 }
2620
2621 // Real-time name validation
2622 const nameInput = document.getElementById('user-name');
2623 if (nameInput) {
2624 let nameValidationTimeout;
2625
2626 nameInput.addEventListener('input', function() {
2627 // Clear previous validation timeout
2628 if (nameValidationTimeout) {
2629 clearTimeout(nameValidationTimeout);
2630 }
2631
2632 // Debounce validation
2633 nameValidationTimeout = setTimeout(() => {
2634 const name = this.value.trim();
2635 clearEmailError();
2636
2637 if (name && !isValidName(name)) {
2638 showEmailError('Name must be between 2 and 100 characters.');
2639 }
2640 }, 500);
2641 });
2642
2643 // Handle Enter key
2644 nameInput.addEventListener('keypress', function(e) {
2645 if (e.key === 'Enter' && !isSubmitting) {
2646 e.preventDefault();
2647 emailForm.dispatchEvent(new Event('submit'));
2648 }
2649 });
2650 }
2651
2652 // Initial state check
2653 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2654 //console.log('Using server-provided email state:', mxchatChat.initial_email_state);
2655
2656 const emailState = mxchatChat.initial_email_state;
2657 if (emailState.show_email_form) {
2658 showEmailForm();
2659 } else {
2660 showChatContainer();
2661 }
2662 } else {
2663 // Check email status via AJAX
2664 setTimeout(checkSessionAndEmail, 100);
2665 }
2666
2667 // Check if email exists for the current session
2668 function checkSessionAndEmail() {
2669 const sessionId = getChatSession();
2670
2671 fetch(mxchatChat.ajax_url, {
2672 method: 'POST',
2673 headers: {
2674 'Content-Type': 'application/x-www-form-urlencoded',
2675 },
2676 body: new URLSearchParams({
2677 action: 'mxchat_check_email_provided',
2678 session_id: sessionId,
2679 nonce: mxchatChat.nonce,
2680 })
2681 })
2682 .then((response) => {
2683 if (!response.ok) {
2684 throw new Error(`HTTP error! status: ${response.status}`);
2685 }
2686 return response.json();
2687 })
2688 .then((data) => {
2689 if (data.success) {
2690 if (data.data.logged_in || data.data.email) {
2691 showChatContainer();
2692 } else {
2693 showEmailForm();
2694 }
2695 } else {
2696 // On error, default to showing email form
2697 showEmailForm();
2698 }
2699 })
2700 .catch((error) => {
2701 console.warn('Email check failed, defaulting to email form:', error);
2702 showEmailForm();
2703 });
2704 }
2705
2706 } else {
2707 console.error('Email collection is enabled but essential elements are missing:', {
2708 emailForm: !!emailForm,
2709 emailBlocker: !!emailBlocker,
2710 chatbotWrapper: !!chatbotWrapper
2711 });
2712 }
2713 } else {
2714 //console.log('Email collection is disabled');
2715 }
2716
2717 // Open chatbot when pre-chat message is clicked
2718 $(document).on('click', '#pre-chat-message', function() {
2719 var chatbot = $('#floating-chatbot');
2720 if (chatbot.hasClass('hidden')) {
2721 chatbot.removeClass('hidden').addClass('visible');
2722 $('#floating-chatbot-button').addClass('hidden');
2723 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
2724 disableScroll(); // Disable scroll when chatbot opens
2725 }
2726 });
2727
2728 var closeButton = document.querySelector('.close-pre-chat-message');
2729 if (closeButton) {
2730 closeButton.addEventListener('click', function() {
2731 $('#pre-chat-message').fadeOut(200); // Hide the message
2732
2733 // Send an AJAX request to set the transient flag for 24 hours
2734 $.ajax({
2735 url: mxchatChat.ajax_url,
2736 type: 'POST',
2737 data: {
2738 action: 'mxchat_dismiss_pre_chat_message',
2739 _ajax_nonce: mxchatChat.nonce
2740 },
2741 success: function() {
2742 //console.log('Pre-chat message dismissed for 24 hours.');
2743
2744 // Ensure the message is hidden after dismissal
2745 $('#pre-chat-message').hide();
2746 },
2747 error: function() {
2748 ////console.error('Failed to dismiss pre-chat message.');
2749 }
2750 });
2751 });
2752 }
2753
2754
2755 function hasQuickQuestions() {
2756 const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
2757 return questionButtons.length > 0;
2758 }
2759
2760 function collapseQuickQuestions() {
2761 const questionsContainer = document.getElementById('mxchat-popular-questions');
2762 if (questionsContainer && hasQuickQuestions()) {
2763 questionsContainer.classList.add('collapsed');
2764 questionsContainer.classList.add('has-been-collapsed');
2765 try {
2766 sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2767 sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
2768 } catch (e) {
2769 // Ignore if sessionStorage is not available
2770 }
2771 }
2772 }
2773
2774 function expandQuickQuestions() {
2775 const questionsContainer = document.getElementById('mxchat-popular-questions');
2776 if (questionsContainer && hasQuickQuestions()) {
2777 questionsContainer.classList.remove('collapsed');
2778 try {
2779 sessionStorage.setItem('mxchat_questions_collapsed', 'false');
2780 } catch (e) {
2781 // Ignore if sessionStorage is not available
2782 }
2783 }
2784 }
2785
2786 function checkQuickQuestionsState() {
2787 if (!hasQuickQuestions()) {
2788 return; // Don't do anything if no questions exist
2789 }
2790
2791 try {
2792 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2793 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2794
2795 const questionsContainer = document.getElementById('mxchat-popular-questions');
2796 if (questionsContainer) {
2797 if (hasBeenCollapsed === 'true') {
2798 questionsContainer.classList.add('has-been-collapsed');
2799 }
2800 if (isCollapsed === 'true') {
2801 questionsContainer.classList.add('collapsed');
2802 }
2803 }
2804 } catch (e) {
2805 // Ignore if sessionStorage is not available
2806 }
2807 }
2808
2809 // Global delegation for dynamically added links as fallback
2810 $(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
2811 const $link = $(this);
2812 const messageDiv = $link.closest('.bot-message, .agent-message');
2813
2814 // Only process bot/agent message links
2815 if (messageDiv.length > 0) {
2816 const originalHref = $link.attr('href');
2817
2818 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2819 e.preventDefault();
2820 e.stopPropagation();
2821
2822 // Mark as tracked
2823 $link.attr('data-tracked', 'true');
2824
2825 // Get message context from the message div
2826 const messageText = messageDiv.text().substring(0, 200);
2827
2828 $.ajax({
2829 url: mxchatChat.ajax_url,
2830 type: 'POST',
2831 data: {
2832 action: 'mxchat_track_url_click',
2833 session_id: getChatSession(),
2834 url: originalHref,
2835 message_context: messageText,
2836 nonce: mxchatChat.nonce
2837 },
2838 complete: function() {
2839 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2840 window.open(originalHref, '_blank');
2841 } else {
2842 window.location.href = originalHref;
2843 }
2844 }
2845 });
2846
2847 return false;
2848 }
2849 }
2850 });
2851
2852
2853
2854
2855 // ====================================
2856 // MAIN INITIALIZATION
2857 // ====================================
2858
2859 if ($('#floating-chatbot').hasClass('hidden')) {
2860 $('#floating-chatbot-button').removeClass('hidden');
2861 }
2862 // Initialize when document is ready
2863 setFullHeight();
2864 initializeChatVisibility();
2865 loadChatHistory();
2866 trackOriginatingPage();
2867
2868 // Make functions globally available for add-ons
2869 window.hasQuickQuestions = hasQuickQuestions;
2870 window.collapseQuickQuestions = collapseQuickQuestions;
2871 window.appendMessage = appendMessage;
2872 window.appendThinkingMessage = appendThinkingMessage;
2873 window.scrollToBottom = scrollToBottom;
2874 window.scrollElementToTop = scrollElementToTop;
2875 window.replaceLastMessage = replaceLastMessage;
2876 window.callMxChat = callMxChat;
2877 window.callMxChatStream = callMxChatStream;
2878 window.shouldUseStreaming = shouldUseStreaming;
2879 window.getChatSession = getChatSession;
2880 window.getPageContext = getPageContext;
2881 window.updateStreamingMessage = updateStreamingMessage;
2882
2883 }); // End of jQuery ready
2884
2885
2886 // ====================================
2887 // GLOBAL EVENT LISTENERS (Outside jQuery)
2888 // ====================================
2889
2890 // Event listener for copy button (code blocks)
2891 document.addEventListener("click", (e) => {
2892 if (e.target.classList.contains("mxchat-copy-button")) {
2893 const copyButton = e.target;
2894 const codeBlock = copyButton
2895 .closest(".mxchat-code-block-container")
2896 .querySelector(".mxchat-code-block code");
2897
2898 if (codeBlock) {
2899 // Preserve formatting using innerText
2900 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
2901 copyButton.textContent = "Copied!";
2902 copyButton.setAttribute("aria-label", "Copied to clipboard");
2903
2904 setTimeout(() => {
2905 copyButton.textContent = "Copy";
2906 copyButton.setAttribute("aria-label", "Copy to clipboard");
2907 }, 2000);
2908 });
2909 }
2910 }
2911 });
2912
2913