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

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

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