PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.3
MxChat – AI Chatbot & Content Generation for WordPress v2.6.3
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
← All changes | js/chat-script.js +898 -2170 3.2.82.6.3 View file →
@@ -1,316 +1,12 @@
1 1 jQuery(document).ready(function($) {
2 -
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
55 - if (callback) callback();
56 - return;
57 - }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
61 - if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
100 - }
101 -
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
2 +
108 3 // ====================================
109 - // MULTI-INSTANCE MANAGEMENT SYSTEM
110 - // ====================================
111 -
112 - // Instance registry - tracks all chatbot instances on the page
113 - const MxChatInstances = {
114 - instances: {},
115 -
116 - // Initialize an instance for a bot
117 - init: function(botId) {
118 - if (!this.instances[botId]) {
119 - // When persistence is OFF, track when this session started
120 - // so the AI only sees messages from this page load
121 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
122 -
123 - this.instances[botId] = {
124 - botId: botId,
125 - sessionId: null,
126 - lastSeenMessageId: '',
127 - notificationCheckInterval: null,
128 - pollingInterval: null,
129 - processedMessageIds: new Set(),
130 - activePdfFile: null,
131 - activeWordFile: null,
132 - chatHistoryLoaded: false,
133 - isStreaming: false,
134 - // Fresh context timestamp - only used when persistence is OFF
135 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
136 - };
137 - }
138 - return this.instances[botId];
139 - },
140 -
141 - // Get instance by botId
142 - get: function(botId) {
143 - return this.instances[botId] || this.init(botId);
144 - },
145 -
146 - // Get all active bot IDs
147 - getAllBotIds: function() {
148 - return Object.keys(this.instances);
149 - },
150 -
151 - // Session management per bot
152 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
153 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
154 - getChatSession: function(botId) {
155 - var cookieName = 'mxchat_session_id_' + botId;
156 - var storageKey = 'mxchat_session_id_' + botId;
157 - var sessionId = getCookie(cookieName);
158 -
159 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
160 - if (!sessionId) {
161 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
162 - }
163 -
164 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
165 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
166 - // storage). Without this, ensureSession() can generate and store an ID that
167 - // getChatSession() then can't read back, causing null session_ids on send.
168 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
169 - sessionId = this.instances[botId].sessionId;
170 - }
171 -
172 - // Guard against stored sentinel values that indicate earlier broken writes.
173 - if (sessionId === 'null' || sessionId === 'undefined') {
174 - sessionId = null;
175 - }
176 -
177 - // Re-sync cookie from localStorage if cookie was lost
178 - if (sessionId && !getCookie(cookieName)) {
179 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
180 - }
181 -
182 - return sessionId || null;
183 - },
184 -
185 - // Lazy session initializer — called on first user interaction
186 - ensureSession: function(botId) {
187 - botId = botId || 'default';
188 - var instance = this.instances[botId] || this.init(botId);
189 -
190 - if (instance.sessionId) {
191 - return instance.sessionId;
192 - }
193 -
194 - // Check for existing session from cookie or localStorage
195 - var existingSession = this.getChatSession(botId);
196 -
197 - if (existingSession) {
198 - instance.sessionId = existingSession;
199 - } else {
200 - // Brand new session
201 - var newId = generateSessionId();
202 - this.setChatSession(botId, newId);
203 - instance.sessionId = newId;
204 - }
205 -
206 - // Now that we have a session, do the deferred work
207 - refreshNonceIfNeeded();
208 - trackOriginatingPage();
209 -
210 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
211 - // so we do NOT call it here to avoid a race condition.
212 -
213 - return instance.sessionId;
214 - },
215 -
216 - setChatSession: function(botId, sessionId) {
217 - var cookieName = 'mxchat_session_id_' + botId;
218 - var storageKey = 'mxchat_session_id_' + botId;
219 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
220 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
221 - if (this.instances[botId]) {
222 - this.instances[botId].sessionId = sessionId;
223 - }
224 - },
225 -
226 - resetChatSession: function(botId) {
227 - // Clear old session from localStorage before setting new one
228 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
229 - var newSessionId = generateSessionId();
230 - this.setChatSession(botId, newSessionId);
231 - var $chatBox = getElement(botId, 'chat-box');
232 - if ($chatBox.length) {
233 - $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
234 - }
235 - if (this.instances[botId]) {
236 - this.instances[botId].chatHistoryLoaded = false;
237 - this.instances[botId].processedMessageIds = new Set();
238 - }
239 - },
240 -
241 - // Silent reset — new session ID without clearing the chat UI
242 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
243 - silentResetSession: function(botId) {
244 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
245 - var newSessionId = generateSessionId();
246 - this.setChatSession(botId, newSessionId);
247 - if (this.instances[botId]) {
248 - this.instances[botId].sessionId = newSessionId;
249 - }
250 - return newSessionId;
251 - }
252 - };
253 -
254 - // ====================================
255 - // ELEMENT SELECTOR HELPERS
256 - // ====================================
257 -
258 - // Check if a specific bot has an AI theme assigned (skip inline colors)
259 - function shouldSkipInlineColors(botId) {
260 - // If global AI theme is active, skip inline colors for all bots
261 - if (mxchatChat.skip_inline_colors) {
262 - return true;
263 - }
264 - // Check if this specific bot has a theme assignment
265 - var botAssignments = mxchatChat.bot_theme_assignments || {};
266 - return botAssignments.hasOwnProperty(botId);
267 - }
268 -
269 - // Get element by ID with bot suffix - returns jQuery object
270 - function getElement(botId, elementName) {
271 - return $('#' + elementName + '-' + botId);
272 - }
273 -
274 - // Get element by ID with bot suffix - returns DOM element
275 - function getElementDOM(botId, elementName) {
276 - return document.getElementById(elementName + '-' + botId);
277 - }
278 -
279 - // Get bot ID from any element within a chatbot instance
280 - function getBotIdFromElement(element) {
281 - var $wrapper = $(element).closest('.mxchat-chatbot-wrapper');
282 - if ($wrapper.length) {
283 - return $wrapper.data('bot-id') || 'default';
284 - }
285 - // Fallback: try to find from floating container
286 - var $floating = $(element).closest('.floating-chatbot');
287 - if ($floating.length) {
288 - var id = $floating.attr('id') || '';
289 - var match = id.match(/floating-chatbot-(.+)/);
290 - if (match) return match[1];
291 - }
292 - // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
293 - var elementId = $(element).attr('id') || '';
294 - if (elementId) {
295 - // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
296 - var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+)$/);
297 - if (idMatch) return idMatch[1];
298 - }
299 - return 'default';
300 - }
301 -
302 - // Get wrapper element for a bot
303 - function getWrapper(botId) {
304 - return getElement(botId, 'mxchat-chatbot-wrapper');
305 - }
306 -
307 - // ====================================
308 4 // GLOBAL VARIABLES & CONFIGURATION
309 5 // ====================================
310 6 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
311 -
312 - // Initialize color settings (these are global as they come from PHP)
7 +
8 + // Initialize color settings
313 9 var userMessageBgColor = mxchatChat.user_message_bg_color;
314 10 var userMessageFontColor = mxchatChat.user_message_font_color;
315 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
316 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
@@ -315,64 +11,61 @@
315 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
316 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
317 13 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
318 14 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
319 -
15 +
320 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;
321 26
322 27 // ====================================
323 - // SESSION MANAGEMENT (Legacy compatibility)
28 + // SESSION MANAGEMENT
324 29 // ====================================
30 +
31 + function getChatSession() {
32 + var sessionId = getCookie('mxchat_session_id');
325 33
34 + if (!sessionId) {
35 + sessionId = generateSessionId();
36 + setChatSession(sessionId);
37 + }
38 +
39 + return sessionId;
40 + }
41 +
42 + function setChatSession(sessionId) {
43 + // Set the cookie with a 24-hour expiration (86400 seconds)
44 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
45 + }
46 +
326 47 function getCookie(name) {
327 48 let value = "; " + document.cookie;
328 49 let parts = value.split("; " + name + "=");
329 50 if (parts.length == 2) return parts.pop().split(";").shift();
330 51 }
331 -
52 +
332 53 function generateSessionId() {
333 54 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
334 55 }
335 56
336 - // Legacy function - now delegates to instance manager
337 - function getChatSession(botId) {
338 - botId = botId || 'default';
339 - return MxChatInstances.getChatSession(botId);
57 + function resetChatSession() {
58 + // Generate a new session ID
59 + var newSessionId = generateSessionId();
60 + // Update the cookie with the new session
61 + setChatSession(newSessionId);
62 + // Clear the chat history display
63 + $('.mxchat-messages').empty();
64 + // Reset the chat history loaded flag
65 + chatHistoryLoaded = false;
340 66 }
341 67
342 - function setChatSession(sessionId, botId) {
343 - botId = botId || 'default';
344 - MxChatInstances.setChatSession(botId, sessionId);
345 - }
346 -
347 - function resetChatSession(botId) {
348 - botId = botId || 'default';
349 - MxChatInstances.resetChatSession(botId);
350 - }
351 -
352 - // ====================================
353 - // INITIALIZE ALL CHATBOT INSTANCES
354 - // ====================================
355 -
356 - function initializeAllInstances() {
357 - // Find all chatbot wrappers on the page
358 - $('.mxchat-chatbot-wrapper').each(function() {
359 - var botId = $(this).data('bot-id') || 'default';
360 - MxChatInstances.init(botId);
361 - initializeBotInstance(botId);
362 - });
363 - }
364 -
365 - function initializeBotInstance(botId) {
366 - var instance = MxChatInstances.get(botId);
367 -
368 - // Initialize quick questions state for this bot
369 - checkQuickQuestionsState(botId);
370 -
371 - // Note: Event handlers use event delegation with class selectors,
372 - // so they work automatically for all instances without per-bot setup
373 - }
374 -
375 68 // ====================================
376 69 // CONTEXTUAL AWARENESS FUNCTIONALITY
377 70 // ====================================
378 71
@@ -519,12 +212,11 @@
519 212 // CORE CHAT FUNCTIONALITY
520 213 // ====================================
521 214
522 215 // Helper functions to disable/enable chat input while waiting for response
523 -function disableChatInput(botId) {
524 - botId = botId || 'default';
525 - var chatInput = getElementDOM(botId, 'chat-input');
526 - var sendButton = getElementDOM(botId, 'send-button');
216 +function disableChatInput() {
217 + var chatInput = document.getElementById('chat-input');
218 + var sendButton = document.getElementById('send-button');
527 219 if (chatInput) {
528 220 chatInput.disabled = true;
529 221 chatInput.style.opacity = '0.6';
530 222 }
@@ -534,12 +226,11 @@
534 226 sendButton.style.pointerEvents = 'none';
535 227 }
536 228 }
537 229
538 -function enableChatInput(botId) {
539 - botId = botId || 'default';
540 - var chatInput = getElementDOM(botId, 'chat-input');
541 - var sendButton = getElementDOM(botId, 'send-button');
230 +function enableChatInput() {
231 + var chatInput = document.getElementById('chat-input');
232 + var sendButton = document.getElementById('send-button');
542 233 if (chatInput) {
543 234 chatInput.disabled = false;
544 235 chatInput.style.opacity = '1';
545 236 chatInput.focus();
@@ -551,13 +242,10 @@
551 242 }
552 243 }
553 244
554 245 // Update your existing sendMessage function
555 -function sendMessage(botId) {
556 - botId = botId || 'default';
557 - MxChatInstances.ensureSession(botId);
558 - var $chatInput = getElement(botId, 'chat-input');
559 - var message = $chatInput.val();
246 +function sendMessage() {
247 + var message = $('#chat-input').val();
560 248
561 249 // ADD PROMPT HOOK HERE
562 250 if (typeof customMxChatFilter === 'function') {
563 251 message = customMxChatFilter(message, "prompt");
@@ -563,77 +251,66 @@
563 251 message = customMxChatFilter(message, "prompt");
564 252 }
565 253
566 254 if (message) {
567 - // Don't disable input in live agent mode - let users chat freely
568 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
569 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
570 - if (!isAgentMode) {
571 - disableChatInput(botId);
572 - }
255 + // Disable input while waiting for response
256 + disableChatInput();
573 257
574 - appendMessage("user", message, '', [], false, botId);
575 - $chatInput.val('');
576 - $chatInput.css('height', 'auto');
258 + appendMessage("user", message);
259 + $('#chat-input').val('');
260 + $('#chat-input').css('height', 'auto');
577 261
578 - if (hasQuickQuestions(botId)) {
579 - collapseQuickQuestions(botId);
262 + if (hasQuickQuestions()) {
263 + collapseQuickQuestions();
580 264 }
581 - appendThinkingMessage(botId);
582 - scrollToBottom(botId);
265 + appendThinkingMessage();
266 + scrollToBottom();
583 267
584 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
268 + const currentModel = mxchatChat.model || 'gpt-4o';
585 269
586 270 // Check if streaming is enabled AND supported for this model
587 271 if (shouldUseStreaming(currentModel)) {
588 272 callMxChatStream(message, function(response) {
589 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
590 - }, botId);
273 + $('.bot-message.temporary-message').removeClass('temporary-message');
274 + });
591 275 } else {
592 276 callMxChat(message, function(response) {
593 - replaceLastMessage("bot", response, '', [], botId);
594 - }, botId);
277 + replaceLastMessage("bot", response);
278 + });
595 279 }
596 280 }
597 281 }
598 282
599 283 // Update your existing sendMessageToChatbot function
600 -function sendMessageToChatbot(message, botId) {
601 - botId = botId || 'default';
602 - MxChatInstances.ensureSession(botId);
603 -
284 +function sendMessageToChatbot(message) {
604 285 // ADD PROMPT HOOK HERE
605 286 if (typeof customMxChatFilter === 'function') {
606 287 message = customMxChatFilter(message, "prompt");
607 288 }
608 289
609 - // Don't disable input in live agent mode - let users chat freely
610 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
611 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
612 - if (!isAgentMode) {
613 - disableChatInput(botId);
614 - }
290 + // Disable input while waiting for response
291 + disableChatInput();
615 292
616 - var sessionId = getChatSession(botId);
293 + var sessionId = getChatSession();
617 294
618 - if (hasQuickQuestions(botId)) {
619 - collapseQuickQuestions(botId);
295 + if (hasQuickQuestions()) {
296 + collapseQuickQuestions();
620 297 }
621 - appendThinkingMessage(botId);
622 - scrollToBottom(botId);
298 + appendThinkingMessage();
299 + scrollToBottom();
623 300
624 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
301 + const currentModel = mxchatChat.model || 'gpt-4o';
625 302
626 303 // Check if streaming is enabled AND supported for this model
627 304 if (shouldUseStreaming(currentModel)) {
628 305 callMxChatStream(message, function(response) {
629 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
630 - }, botId);
306 + $('.bot-message.temporary-message').removeClass('temporary-message');
307 + });
631 308 } else {
632 309 callMxChat(message, function(response) {
633 - getElement(botId, 'chat-box').find('.temporary-message').remove();
634 - replaceLastMessage("bot", response, '', [], botId);
635 - }, botId);
310 + $('.temporary-message').remove();
311 + replaceLastMessage("bot", response);
312 + });
636 313 }
637 314 }
638 315
639 316 // Updated shouldUseStreaming function with debugging
@@ -676,65 +353,40 @@
676 353 updateChatModeIndicator('agent');
677 354 }
678 355 }
679 356
680 -// Function to get bot ID from any element or wrapper
681 -// If element is provided, finds the bot ID from its wrapper
682 -// If no element, returns 'default' (for backward compatibility)
683 -function getMxChatBotId(element) {
684 - if (element) {
685 - return getBotIdFromElement(element);
686 - }
687 - // Fallback: find first chatbot wrapper on page
688 - const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper');
357 +//Function to get bot ID from the chatbot wrapper
358 +function getMxChatBotId() {
359 + const chatbotWrapper = document.getElementById('mxchat-chatbot-wrapper');
689 360 return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
690 361 }
691 362
692 -function callMxChat(message, callback, botId) {
693 - botId = botId || getMxChatBotId();
694 -
363 +function callMxChat(message, callback) {
695 364 // Store the message in case we need to retry after session reset
696 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
365 + $('.mxchat-input-holder textarea').data('pending-message', message);
697 366
698 367 // Get page context if contextual awareness is enabled
699 368 const pageContext = getPageContext();
700 369
701 - // Get instance for session start timestamp (used when persistence is OFF)
702 - var instance = MxChatInstances.get(botId);
370 + // Get bot ID
371 + const botId = getMxChatBotId();
703 372
704 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
705 - // and returns the guaranteed-present session id from the in-memory instance even when
706 - // cookie/localStorage writes are silently blocked by the browser.
707 - var sessionId = MxChatInstances.ensureSession(botId);
708 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
709 - // Last-resort generation to ensure we never POST a null marker.
710 - sessionId = generateSessionId();
711 - MxChatInstances.setChatSession(botId, sessionId);
712 - }
713 -
714 - // Wait for the page-cache nonce refresh to complete before firing the
715 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
716 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
717 - // callback guarantees we read the fresh value. See plan-c5457f.
718 - refreshNonceIfNeeded(function() {
719 373 // Prepare AJAX data
720 374 const ajaxData = {
721 375 action: 'mxchat_handle_chat_request',
722 376 message: message,
723 - session_id: sessionId,
377 + session_id: getChatSession(),
724 378 nonce: mxchatChat.nonce,
725 379 current_page_url: window.location.href,
726 380 current_page_title: document.title,
727 - bot_id: botId,
728 - // Pass session start timestamp so AI context matches what user sees
729 - session_start_timestamp: instance.sessionStartTimestamp || 0
381 + bot_id: botId // Include bot ID
730 382 };
731 -
383 +
732 384 // Add page context if available
733 385 if (pageContext) {
734 386 ajaxData.page_context = JSON.stringify(pageContext);
735 387 }
736 -
388 +
737 389 // CHECK FOR VISION FLAGS AND ADD THEM
738 390 if (window.mxchatVisionProcessed) {
739 391 ajaxData.vision_processed = true;
740 392 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -743,9 +395,9 @@
743 395 window.mxchatVisionProcessed = false;
744 396 window.mxchatOriginalMessage = null;
745 397 window.mxchatVisionImagesCount = 0;
746 398 }
747 -
399 +
748 400 $.ajax({
749 401 url: mxchatChat.ajax_url,
750 402 type: 'POST',
751 403 dataType: 'json',
@@ -752,14 +404,14 @@
752 404 data: ajaxData,
753 405 success: function(response) {
754 406 // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
755 407 if (response.chat_mode) {
756 - updateChatModeIndicator(response.chat_mode, botId);
408 + updateChatModeIndicator(response.chat_mode);
757 409 }
758 410
759 411 // Also check in data property if response is wrapped
760 412 if (response.data && response.data.chat_mode) {
761 - updateChatModeIndicator(response.data.chat_mode, botId);
413 + updateChatModeIndicator(response.data.chat_mode);
762 414 }
763 415
764 416 // SECURITY FIX: Check for errors FIRST before checking for success
765 417 // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
@@ -783,24 +435,31 @@
783 435 errorMessage = "An error occurred. Please try again or contact support.";
784 436 }
785 437
786 438 // Handle session reset action (IP changed, session expired, etc.)
787 - // Silent reset — keep chat UI intact, just get a new session and retry
788 439 if (response.data && response.data.action === 'reset_session') {
789 - MxChatInstances.silentResetSession(botId);
790 - // Re-send the original message with the new session (user message is already displayed)
791 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
440 + // Clear the old session and generate a new one
441 + resetChatSession();
442 + // Remove the temporary loading message
443 + $('.bot-message.temporary-message').remove();
444 + // Re-send the original message with the new session
445 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
792 446 if (originalMessage) {
793 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
794 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
447 + $('.mxchat-input-holder textarea').data('pending-message', null);
448 + // Re-add the user message and thinking indicator
449 + appendMessage("user", originalMessage);
450 + appendThinkingMessage();
451 + scrollToBottom();
452 + // Determine whether to use streaming
453 + const currentModel = mxchatChat.model || 'gpt-4o';
795 454 if (shouldUseStreaming(currentModel)) {
796 455 callMxChatStream(originalMessage, function(response) {
797 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
798 - }, botId);
456 + $('.bot-message.temporary-message').removeClass('temporary-message');
457 + });
799 458 } else {
800 459 callMxChat(originalMessage, function(response) {
801 - replaceLastMessage("bot", response, '', [], botId);
802 - }, botId);
460 + replaceLastMessage("bot", response);
461 + });
803 462 }
804 463 }
805 464 return;
806 465 }
@@ -813,9 +472,9 @@
813 472 // For admin users, show more technical details including error code
814 473 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
815 474 }
816 475
817 - replaceLastMessage("bot", displayMessage, '', [], botId);
476 + replaceLastMessage("bot", displayMessage);
818 477 return; // Exit early for errors
819 478 }
820 479
821 480 // NOW check if this is a successful response by looking for text, html, or message fields
@@ -831,17 +490,16 @@
831 490 let responseMessage = response.message || '';
832 491
833 492 // Add PDF filename handling
834 493 if (response.data && response.data.filename) {
835 - showActivePdf(response.data.filename, botId);
836 - var instance = MxChatInstances.get(botId);
837 - instance.activePdfFile = response.data.filename;
494 + showActivePdf(response.data.filename);
495 + activePdfFile = response.data.filename;
838 496 }
839 497
840 498 // Add redirect check here
841 499 if (response.redirect_url) {
842 500 if (responseText) {
843 - replaceLastMessage("bot", responseText, '', [], botId);
501 + replaceLastMessage("bot", responseText);
844 502 }
845 503 setTimeout(() => {
846 504 window.location.href = response.redirect_url;
847 505 }, 1500);
@@ -849,11 +507,9 @@
849 507 }
850 508
851 509 // Check for live agent response
852 510 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
853 - removeThinkingDots(botId);
854 - updateChatModeIndicator('agent', botId);
855 - enableChatInput(botId);
511 + updateChatModeIndicator('agent');
856 512 return;
857 513 }
858 514
859 515 // Handle the message and show notification if chat is hidden
@@ -868,36 +524,30 @@
868 524 }
869 525
870 526 // Update the messages as before
871 527 if (responseText && responseHtml) {
872 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
528 + replaceLastMessage("bot", responseText, responseHtml);
873 529 } else if (responseText) {
874 - replaceLastMessage("bot", responseText, '', [], botId);
530 + replaceLastMessage("bot", responseText);
875 531 } else if (responseHtml) {
876 - replaceLastMessage("bot", "", responseHtml, [], botId);
532 + replaceLastMessage("bot", "", responseHtml);
877 533 } else if (responseMessage) {
878 - replaceLastMessage("bot", responseMessage, '', [], botId);
534 + replaceLastMessage("bot", responseMessage);
879 535 }
880 536
881 537 // Check if chat is hidden and show notification
882 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
883 - if ($floatingChatbot.hasClass('hidden')) {
884 - var $badge = getElement(botId, 'chat-notification-badge');
885 - if ($badge.length) {
886 - $badge.show();
538 + if ($('#floating-chatbot').hasClass('hidden')) {
539 + const badge = $('#chat-notification-badge');
540 + if (badge.length) {
541 + badge.show();
887 542 }
888 543 }
889 544 } else {
890 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
891 - if (response.vectorstore_error) {
892 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
893 - }
894 - replaceLastMessage("bot", emptyMsg, '', [], botId);
545 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
895 546 }
896 547
897 548 if (response.message_id) {
898 - var instance = MxChatInstances.get(botId);
899 - instance.lastSeenMessageId = response.message_id;
549 + lastSeenMessageId = response.message_id;
900 550 }
901 551
902 552 return;
903 553 }
@@ -902,9 +552,9 @@
902 552 return;
903 553 }
904 554
905 555 // Fallback for truly unexpected response formats
906 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
556 + replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.");
907 557 },
908 558 error: function(xhr, status, error) {
909 559 let errorMessage = "An unexpected error occurred.";
910 560
@@ -931,56 +581,38 @@
931 581 errorMessage = "Server error: The server encountered an issue. Please try again later.";
932 582 }
933 583 }
934 584
935 - replaceLastMessage("bot", errorMessage, '', [], botId);
585 + replaceLastMessage("bot", errorMessage);
936 586 }
937 587 });
938 - }); // refreshNonceIfNeeded
939 588 }
940 589
941 -function callMxChatStream(message, callback, botId) {
942 - botId = botId || getMxChatBotId();
943 -
590 +function callMxChatStream(message, callback) {
944 591 // Store the message in case we need to retry after session reset
945 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
592 + $('.mxchat-input-holder textarea').data('pending-message', message);
946 593
947 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
594 + const currentModel = mxchatChat.model || 'gpt-4o';
948 595 if (!isStreamingSupported(currentModel)) {
949 - callMxChat(message, callback, botId);
596 + callMxChat(message, callback);
950 597 return;
951 598 }
952 599
953 600 // Get page context if contextual awareness is enabled
954 601 const pageContext = getPageContext();
602 +
603 + // Get bot ID
604 + const botId = getMxChatBotId();
955 605
956 - // Get instance for session start timestamp (used when persistence is OFF)
957 - var instance = MxChatInstances.get(botId);
958 -
959 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
960 - // non-string value via String(), so passing `null` would POST the literal string "null"
961 - // and land in the transcripts table as a ghost session. ensureSession() always returns
962 - // a real string even when cookies/localStorage are blocked.
963 - var streamSessionId = MxChatInstances.ensureSession(botId);
964 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
965 - streamSessionId = generateSessionId();
966 - MxChatInstances.setChatSession(botId, streamSessionId);
967 - }
968 -
969 - // Wait for the page-cache nonce refresh before constructing formData (which
970 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
971 - refreshNonceIfNeeded(function() {
972 606 const formData = new FormData();
973 607 formData.append('action', 'mxchat_stream_chat');
974 608 formData.append('message', message);
975 - formData.append('session_id', streamSessionId);
609 + formData.append('session_id', getChatSession());
976 610 formData.append('nonce', mxchatChat.nonce);
977 611 formData.append('current_page_url', window.location.href);
978 612 formData.append('current_page_title', document.title);
979 - formData.append('bot_id', botId);
980 - // Pass session start timestamp so AI context matches what user sees
981 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
982 -
613 + formData.append('bot_id', botId); // Include bot ID
614 +
983 615 // Add page context if available
984 616 if (pageContext) {
985 617 formData.append('page_context', JSON.stringify(pageContext));
986 618 }
@@ -1023,9 +655,9 @@
1023 655 if (contentType && contentType.includes('application/json')) {
1024 656 return responseClone.json().then(data => {
1025 657 // IMMEDIATE CHAT MODE UPDATE for JSON response
1026 658 if (data.chat_mode) {
1027 - updateChatModeIndicator(data.chat_mode, botId);
659 + updateChatModeIndicator(data.chat_mode);
1028 660 }
1029 661
1030 662 // Check for testing panel
1031 663 if (window.mxchatTestPanelInstance && data.testing_data) {
@@ -1032,9 +664,9 @@
1032 664 window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
1033 665 }
1034 666
1035 667 // Handle the JSON response directly
1036 - handleNonStreamResponse(data, callback, botId);
668 + handleNonStreamResponse(data, callback);
1037 669 return Promise.resolve(); // Prevent further processing
1038 670 });
1039 671 }
1040 672
@@ -1052,37 +684,26 @@
1052 684 responseClone.text().then(text => {
1053 685 try {
1054 686 const data = JSON.parse(text);
1055 687 if (data.text || data.message || data.html) {
1056 - handleNonStreamResponse(data, callback, botId);
688 + handleNonStreamResponse(data, callback);
1057 689 } else {
1058 690 // No valid data, fall back to regular call
1059 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1060 - callMxChat(message, callback, botId);
691 + $('.bot-message.temporary-message').remove();
692 + callMxChat(message, callback);
1061 693 }
1062 694 } catch (e) {
1063 695 // Could not parse, fall back to regular call
1064 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1065 - callMxChat(message, callback, botId);
696 + $('.bot-message.temporary-message').remove();
697 + callMxChat(message, callback);
1066 698 }
1067 699 }).catch(() => {
1068 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1069 - callMxChat(message, callback, botId);
700 + $('.bot-message.temporary-message').remove();
701 + callMxChat(message, callback);
1070 702 });
1071 703 return;
1072 704 }
1073 -
1074 - // Re-enable chat input when stream ends with content
1075 - enableChatInput(botId);
1076 -
1077 - // Scroll the user's last message to the top now that the
1078 - // bot's full reply has rendered (gives max reading room).
1079 - var $chatBoxDone = getElement(botId, 'chat-box');
1080 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1081 - if ($lastUserMsgDone.length) {
1082 - scrollElementToTop($lastUserMsgDone, botId);
1083 - }
1084 -
705 +
1085 706 if (callback) {
1086 707 callback(accumulatedContent);
1087 708 }
1088 709 return;
@@ -1097,24 +718,16 @@
1097 718 const data = line.substring(6);
1098 719
1099 720 if (data === '[DONE]') {
1100 721 if (!accumulatedContent) {
1101 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1102 - callMxChat(message, callback, botId);
722 + $('.bot-message.temporary-message').remove();
723 + callMxChat(message, callback);
1103 724 return;
1104 725 }
1105 726
1106 727 // Re-enable chat input after streaming completes
1107 - enableChatInput(botId);
728 + enableChatInput();
1108 729
1109 - // Scroll the user's last message to the top now
1110 - // that the bot's full reply has rendered.
1111 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1112 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1113 - if ($lastUserMsgStreamDone.length) {
1114 - scrollElementToTop($lastUserMsgStreamDone, botId);
1115 - }
1116 -
1117 730 if (callback) {
1118 731 callback(accumulatedContent);
1119 732 }
1120 733 return;
@@ -1124,9 +737,9 @@
1124 737 const json = JSON.parse(data);
1125 738
1126 739 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
1127 740 if (json.chat_mode) {
1128 - updateChatModeIndicator(json.chat_mode, botId);
741 + updateChatModeIndicator(json.chat_mode);
1129 742 }
1130 743
1131 744 // Handle testing data
1132 745 if (json.testing_data && !testingDataReceived) {
@@ -1138,13 +751,13 @@
1138 751 // Handle content streaming
1139 752 else if (json.content) {
1140 753 streamingStarted = true;
1141 754 accumulatedContent += json.content;
1142 - updateStreamingMessage(accumulatedContent, botId);
755 + updateStreamingMessage(accumulatedContent);
1143 756 }
1144 757 // Handle complete response in stream (fallback response)
1145 758 else if (json.text || json.message || json.html) {
1146 - handleNonStreamResponse(json, callback, botId);
759 + handleNonStreamResponse(json, callback);
1147 760 return;
1148 761 }
1149 762 // Handle errors
1150 763 else if (json.error) {
@@ -1153,12 +766,12 @@
1153 766 let errorMessage = json.error_message || json.message || json.text ||
1154 767 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1155 768
1156 769 // Re-enable chat input on error
1157 - enableChatInput(botId);
770 + enableChatInput();
1158 771
1159 772 // Display the error directly in the chat
1160 - replaceLastMessage("bot", errorMessage, '', [], botId);
773 + replaceLastMessage("bot", errorMessage);
1161 774
1162 775 if (callback) {
1163 776 callback(errorMessage);
1164 777 }
@@ -1171,10 +784,10 @@
1171 784 }
1172 785
1173 786 processStream();
1174 787 }).catch(streamError => {
1175 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1176 - callMxChat(message, callback, botId);
788 + $('.bot-message.temporary-message').remove();
789 + callMxChat(message, callback);
1177 790 });
1178 791 }
1179 792
1180 793 processStream();
@@ -1183,33 +796,30 @@
1183 796 // Check if we have server error data with chat mode
1184 797 if (error && error.isServerError && error.data) {
1185 798 // Check for chat mode in error data
1186 799 if (error.data.chat_mode) {
1187 - updateChatModeIndicator(error.data.chat_mode, botId);
800 + updateChatModeIndicator(error.data.chat_mode);
1188 801 }
1189 802
1190 - handleNonStreamResponse(error.data, callback, botId);
803 + handleNonStreamResponse(error.data, callback);
1191 804 } else {
1192 805 // Only fall back to regular call if we don't have any response data
1193 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1194 - callMxChat(message, callback, botId);
806 + $('.bot-message.temporary-message').remove();
807 + callMxChat(message, callback);
1195 808 }
1196 809 });
1197 - }); // refreshNonceIfNeeded
1198 810 }
1199 811
1200 812 // Helper function to handle non-streaming responses
1201 -function handleNonStreamResponse(data, callback, botId) {
1202 - botId = botId || 'default';
1203 -
813 +function handleNonStreamResponse(data, callback) {
1204 814 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1205 815 if (data.chat_mode) {
1206 - updateChatModeIndicator(data.chat_mode, botId);
816 + updateChatModeIndicator(data.chat_mode);
1207 817 }
1208 818
1209 819 // Also check in data property if response is wrapped
1210 820 if (data.data && data.data.chat_mode) {
1211 - updateChatModeIndicator(data.data.chat_mode, botId);
821 + updateChatModeIndicator(data.data.chat_mode);
1212 822 }
1213 823
1214 824 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1215 825 // This prevents a visual gap between thinking dots disappearing and content appearing
@@ -1234,20 +844,25 @@
1234 844 errorMessage = "An error occurred. Please try again or contact support.";
1235 845 }
1236 846
1237 847 // Handle session reset action (IP changed, session expired, etc.)
1238 - // Silent reset — keep chat UI intact, just get a new session and retry
1239 848 if (data.data && data.data.action === 'reset_session') {
1240 - MxChatInstances.silentResetSession(botId);
1241 - // Re-send the original message with the new session (user message is already displayed)
1242 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
849 + // Clear the old session and generate a new one
850 + resetChatSession();
851 + // Re-send the original message with the new session
852 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
1243 853 if (originalMessage) {
1244 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1245 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
854 + $('.mxchat-input-holder textarea').data('pending-message', null);
855 + // Re-add the user message and thinking indicator
856 + appendMessage("user", originalMessage);
857 + appendThinkingMessage();
858 + scrollToBottom();
859 + // Determine whether to use streaming
860 + const currentModel = mxchatChat.model || 'gpt-4o';
1246 861 if (shouldUseStreaming(currentModel)) {
1247 - callMxChatStream(originalMessage, callback, botId);
862 + callMxChatStream(originalMessage, callback);
1248 863 } else {
1249 - callMxChat(originalMessage, callback, botId);
864 + callMxChat(originalMessage, callback);
1250 865 }
1251 866 }
1252 867 return;
1253 868 }
@@ -1257,9 +872,9 @@
1257 872 if (mxchatChat.is_admin) {
1258 873 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1259 874 }
1260 875
1261 - replaceLastMessage("bot", displayMessage, '', [], botId);
876 + replaceLastMessage("bot", displayMessage);
1262 877
1263 878 if (callback) {
1264 879 callback('');
1265 880 }
@@ -1265,22 +880,8 @@
1265 880 }
1266 881 return; // Exit early for errors
1267 882 }
1268 883
1269 - // Check for live agent response
1270 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1271 - removeThinkingDots(botId);
1272 - // Also remove any leftover bot-message that lost its temporary-message class
1273 - var $chatBox = getElement(botId, 'chat-box');
1274 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1275 - updateChatModeIndicator('agent', botId);
1276 - enableChatInput(botId);
1277 - if (callback) {
1278 - callback('');
1279 - }
1280 - return;
1281 - }
1282 -
1283 884 // Handle different response formats
1284 885 if (data.text || data.html || data.message) {
1285 886
1286 887 // Apply response hooks
@@ -1292,23 +893,22 @@
1292 893 }
1293 894
1294 895 // Display the response
1295 896 if (data.text && data.html) {
1296 - replaceLastMessage("bot", data.text, data.html, [], botId);
897 + replaceLastMessage("bot", data.text, data.html);
1297 898 } else if (data.text) {
1298 - replaceLastMessage("bot", data.text, '', [], botId);
899 + replaceLastMessage("bot", data.text);
1299 900 } else if (data.html) {
1300 - replaceLastMessage("bot", "", data.html, [], botId);
901 + replaceLastMessage("bot", "", data.html);
1301 902 } else if (data.message) {
1302 - replaceLastMessage("bot", data.message, '', [], botId);
903 + replaceLastMessage("bot", data.message);
1303 904 }
1304 905 }
1305 906
1306 907 // Handle other response properties
1307 908 if (data.data && data.data.filename) {
1308 - showActivePdf(data.data.filename, botId);
1309 - var instance = MxChatInstances.get(botId);
1310 - instance.activePdfFile = data.data.filename;
909 + showActivePdf(data.data.filename);
910 + activePdfFile = data.data.filename;
1311 911 }
1312 912
1313 913 if (data.redirect_url) {
1314 914 setTimeout(() => {
@@ -1316,9 +916,9 @@
1316 916 }, 1500);
1317 917 }
1318 918
1319 919 // Ensure chat input is re-enabled (safety net for edge cases)
1320 - enableChatInput(botId);
920 + enableChatInput();
1321 921
1322 922 if (callback) {
1323 923 callback(data.text || data.message || '');
1324 924 }
@@ -1324,22 +924,21 @@
1324 924 }
1325 925 }
1326 926
1327 927 // Enhanced updateChatModeIndicator function for immediate DOM updates
1328 -function updateChatModeIndicator(mode, botId) {
1329 - botId = botId || 'default';
1330 - const indicator = getElementDOM(botId, 'chat-mode-indicator');
928 +function updateChatModeIndicator(mode) {
929 + const indicator = document.getElementById('chat-mode-indicator');
1331 930 if (indicator) {
1332 931 const oldText = indicator.textContent;
1333 932
1334 933 if (mode === 'agent') {
1335 934 indicator.textContent = 'Live Agent';
1336 - startPolling(botId);
935 + startPolling();
1337 936 } else {
1338 937 // Everything else is AI mode
1339 938 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1340 939 indicator.textContent = customAiText;
1341 - stopPolling(botId);
940 + stopPolling();
1342 941 }
1343 942
1344 943 // Force immediate DOM update and reflow
1345 944 if (oldText !== indicator.textContent) {
@@ -1361,21 +960,18 @@
1361 960 }
1362 961 }
1363 962
1364 963 // Function to update message during streaming
1365 -function updateStreamingMessage(content, botId) {
1366 - botId = botId || 'default';
1367 -
964 +function updateStreamingMessage(content) {
1368 965 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1369 966 if (typeof customMxChatFilter === 'function') {
1370 967 content = customMxChatFilter(content, "response");
1371 968 }
1372 -
969 +
1373 970 const formattedContent = linkify(content);
1374 971
1375 - // Find the temporary message in this bot's chat box
1376 - var $chatBox = getElement(botId, 'chat-box');
1377 - const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
972 + // Find the temporary message
973 + const tempMessage = $('.bot-message.temporary-message').last();
1378 974
1379 975 if (tempMessage.length) {
1380 976 // Update existing message
1381 977 tempMessage.html(formattedContent);
@@ -1380,9 +976,9 @@
1380 976 // Update existing message
1381 977 tempMessage.html(formattedContent);
1382 978 } else {
1383 979 // Create new temporary message if it doesn't exist
1384 - appendMessage("bot", content, '', [], true, botId);
980 + appendMessage("bot", content, '', [], true);
1385 981 }
1386 982 }
1387 983
1388 984 function isStreamingSupported(model) {
@@ -1401,253 +997,24 @@
1401 997 return isSupported;
1402 998 }
1403 999
1404 1000 // Update the event handlers to use the correct function names (using event delegation)
1405 -// Use class-based selectors for multi-instance support
1406 -$(document).on('click', '.send-button', function() {
1407 - var botId = getBotIdFromElement(this);
1408 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1409 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1410 - disableChatInput(botId);
1411 - }
1412 - sendMessage(botId);
1001 +$(document).on('click', '#send-button', function() {
1002 + disableChatInput();
1003 + sendMessage();
1413 1004 });
1414 1005
1415 1006 // Override enter key handler (using event delegation)
1416 -$(document).on('keypress', '.chat-input', function(e) {
1007 +$(document).on('keypress', '#chat-input', function(e) {
1417 1008 if (e.which == 13 && !e.shiftKey) {
1418 1009 e.preventDefault();
1419 - var botId = getBotIdFromElement(this);
1420 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1421 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1422 - disableChatInput(botId);
1423 - }
1424 - sendMessage(botId);
1010 + disableChatInput();
1011 + sendMessage();
1425 1012 }
1426 1013 });
1427 1014
1428 -// Builds the list of overflow-menu items for a given bot.
1429 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1430 -function mxchatGetHeaderMenuItems(botId) {
1431 - var items = [];
1432 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1433 -
1434 - // The `print_button_*` keys still gate this item for back-compat with
1435 - // existing user options. The action is now a transcript download, not print.
1436 - if (settings.print_button_enabled === 'on') {
1437 - items.push({
1438 - id: 'download-transcript',
1439 - label: settings.print_button_label || 'Download Transcript',
1440 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1441 - action: function() {
1442 - mxchatDownloadTranscript(botId);
1443 - }
1444 - });
1445 - }
1446 -
1447 - return items;
1448 -}
1449 -
1450 -// Builds a clean markdown transcript of the current conversation and triggers
1451 -// a file download. Used by the "Download Transcript" menu item.
1452 -function mxchatDownloadTranscript(botId) {
1453 - var $chatBox = getElement(botId, 'chat-box');
1454 - if (!$chatBox || !$chatBox.length) return;
1455 -
1456 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1457 - var headerTitle = settings.print_header_title || 'Chat transcript';
1458 - var now = new Date();
1459 - var stamp = now.toLocaleString();
1460 -
1461 - var lines = [];
1462 - lines.push('# ' + headerTitle);
1463 - lines.push('');
1464 - lines.push('Exported: ' + stamp);
1465 - lines.push('');
1466 - lines.push('---');
1467 - lines.push('');
1468 -
1469 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1470 - var $msg = $(this);
1471 - // Skip thinking placeholders and any in-flight temporary messages.
1472 - if ($msg.find('.thinking-dots').length) return;
1473 - if ($msg.hasClass('temporary-message')) return;
1474 -
1475 - var sender;
1476 - if ($msg.hasClass('user-message')) sender = 'User';
1477 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1478 - else sender = 'AI Agent';
1479 -
1480 - // Strip interactive UI from the cloned message so we get the conversation text.
1481 - var $clone = $msg.clone();
1482 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1483 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1484 - if (!text) return;
1485 -
1486 - lines.push('**' + sender + '**');
1487 - lines.push('');
1488 - lines.push(text);
1489 - lines.push('');
1490 - });
1491 -
1492 - var content = lines.join('\n');
1493 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1494 - var fname = 'mxchat-transcript-' + iso + '.md';
1495 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1496 - var url = URL.createObjectURL(blob);
1497 - var a = document.createElement('a');
1498 - a.href = url;
1499 - a.download = fname;
1500 - a.style.display = 'none';
1501 - document.body.appendChild(a);
1502 - a.click();
1503 - setTimeout(function() {
1504 - if (a.parentNode) a.parentNode.removeChild(a);
1505 - URL.revokeObjectURL(url);
1506 - }, 100);
1507 -}
1508 -
1509 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1510 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1511 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1512 -function mxchatSyncMenuColors(botId, $wrap) {
1513 - if (!$wrap || !$wrap.length) return;
1514 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1515 - if (!$bot.length) return;
1516 - var cs = window.getComputedStyle($bot[0]);
1517 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1518 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1519 - }
1520 - // Bot text color usually lives on a child div, not .bot-message itself.
1521 - var $textChild = $bot.find('[style*="color"]').first();
1522 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1523 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1524 -}
1525 -
1526 -// One-time per-widget init: renders menu items, wires open/close,
1527 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1528 -function mxchatInitHeaderMenu(botId) {
1529 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1530 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1531 -
1532 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1533 - var $menu = $wrap.find('.mxchat-header-menu');
1534 - var items = mxchatGetHeaderMenuItems(botId);
1535 -
1536 - // Initial color sync — covers normal page load.
1537 - mxchatSyncMenuColors(botId, $wrap);
1538 -
1539 - if (!items.length) {
1540 - $trigger.hide();
1541 - $menu.hide();
1542 - $wrap.data('mxchatMenuReady', true);
1543 - return;
1544 - }
1545 -
1546 - // Build the menu items.
1547 - $menu.empty();
1548 - items.forEach(function(item, idx) {
1549 - var $btn = $('<button>', {
1550 - type: 'button',
1551 - 'class': 'mxchat-menu-item',
1552 - 'role': 'menuitem',
1553 - 'tabindex': '-1',
1554 - 'data-menu-id': item.id,
1555 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1556 - '<span class="mxchat-menu-item-label"></span>'
1557 - });
1558 - $btn.find('.mxchat-menu-item-label').text(item.label);
1559 - $btn.on('click', function(e) {
1560 - e.preventDefault();
1561 - e.stopPropagation();
1562 - closeMenu();
1563 - try { item.action(); } catch (err) { /* no-op */ }
1564 - });
1565 - $menu.append($btn);
1566 - });
1567 -
1568 - function openMenu() {
1569 - // Re-sync each open in case the active theme changed since init.
1570 - mxchatSyncMenuColors(botId, $wrap);
1571 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1572 - $trigger.attr('aria-expanded', 'true');
1573 - // Focus the first item for keyboard users
1574 - setTimeout(function() {
1575 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1576 - }, 0);
1577 - }
1578 - function closeMenu(returnFocus) {
1579 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1580 - $trigger.attr('aria-expanded', 'false');
1581 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1582 - if (returnFocus) $trigger.trigger('focus');
1583 - }
1584 -
1585 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1586 - // click-to-collapse handler does not fire.
1587 - $trigger.on('click', function(e) {
1588 - e.preventDefault();
1589 - e.stopPropagation();
1590 - if ($menu.hasClass('is-open')) closeMenu();
1591 - else openMenu();
1592 - });
1593 -
1594 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1595 - $menu.on('click', function(e) {
1596 - e.stopPropagation();
1597 - });
1598 -
1599 - // Outside click closes the menu.
1600 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1601 - if (!$menu.hasClass('is-open')) return;
1602 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1603 - closeMenu();
1604 - });
1605 -
1606 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1607 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1608 - var $items = $menu.find('.mxchat-menu-item');
1609 - var idx = $items.index(this);
1610 - if (e.key === 'Escape') {
1611 - e.preventDefault();
1612 - closeMenu(true);
1613 - } else if (e.key === 'ArrowDown') {
1614 - e.preventDefault();
1615 - var $next = $items.eq((idx + 1) % $items.length);
1616 - $items.attr('tabindex', '-1');
1617 - $next.attr('tabindex', '0').trigger('focus');
1618 - } else if (e.key === 'ArrowUp') {
1619 - e.preventDefault();
1620 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1621 - $items.attr('tabindex', '-1');
1622 - $prev.attr('tabindex', '0').trigger('focus');
1623 - } else if (e.key === 'Enter' || e.key === ' ') {
1624 - e.preventDefault();
1625 - $(this).trigger('click');
1626 - }
1627 - });
1628 - $trigger.on('keydown', function(e) {
1629 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1630 - e.preventDefault();
1631 - closeMenu(true);
1632 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1633 - e.preventDefault();
1634 - openMenu();
1635 - }
1636 - });
1637 -
1638 - $wrap.data('mxchatMenuReady', true);
1639 -}
1640 -
1641 -// Initialize header menus for every rendered widget on DOM ready.
1642 -$(function() {
1643 - $('.mxchat-header-menu-wrap').each(function() {
1644 - var botId = $(this).data('bot-id');
1645 - if (botId) mxchatInitHeaderMenu(botId);
1646 - });
1647 -});
1648 -
1649 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1015 +
1016 +function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
1650 1017 try {
1651 1018 // Determine styles based on sender type
1652 1019 let messageClass, bgColor, fontColor;
1653 1020
@@ -1668,29 +1035,25 @@
1668 1035 }
1669 1036
1670 1037 const messageDiv = $('<div>')
1671 1038 .addClass(messageClass)
1672 - .attr('dir', 'auto');
1673 -
1674 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1675 - var skipColors = shouldSkipInlineColors(botId);
1676 - if (skipColors) {
1677 - messageDiv.css({
1678 - 'margin-bottom': '1em'
1679 - });
1680 - } else {
1681 - messageDiv.css({
1039 + .attr('dir', 'auto')
1040 + .css({
1682 1041 'background': bgColor,
1683 1042 'color': fontColor,
1684 1043 'margin-bottom': '1em'
1685 1044 });
1045 +
1046 + // Process the message content based on sender
1047 + let fullMessage;
1048 + if (sender === "user") {
1049 + // For user messages, apply linkify after sanitization
1050 + fullMessage = linkify(messageText);
1051 + } else {
1052 + // For bot/agent messages, preserve HTML
1053 + fullMessage = messageText;
1686 1054 }
1687 1055
1688 - // Process the message content - always run linkify to convert markdown
1689 - // links and format text. linkify() handles existing HTML safely via
1690 - // negative lookaheads that skip URLs already inside <a> tags.
1691 - let fullMessage = linkify(messageText);
1692 -
1693 1056 // Add images if provided
1694 1057 if (images && images.length > 0) {
1695 1058 fullMessage += '<div class="image-gallery" dir="auto">';
1696 1059 images.forEach(img => {
@@ -1696,9 +1059,9 @@
1696 1059 images.forEach(img => {
1697 1060 const safeTitle = sanitizeUserInput(img.title);
1698 1061 const safeUrl = encodeURI(img.image_url);
1699 1062 const safeThumbnail = encodeURI(img.thumbnail_url);
1700 -
1063 +
1701 1064 fullMessage += `
1702 1065 <div style="margin-bottom: 10px;">
1703 1066 <strong>${safeTitle}</strong><br>
1704 1067 <a href="${safeUrl}" target="_blank">
@@ -1724,32 +1087,25 @@
1724 1087 if (isTemporary) {
1725 1088 messageDiv.addClass('temporary-message');
1726 1089 }
1727 1090
1728 - // Append to the correct chatbot instance's chat-box
1729 - var $chatBox = getElement(botId, 'chat-box');
1730 - messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1091 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1731 1092 // FIXED: Use event delegation for link tracking
1732 1093 if (sender === "bot" || sender === "agent") {
1733 - attachLinkTracking(messageDiv, messageText, botId);
1094 + attachLinkTracking(messageDiv, messageText);
1734 1095 }
1735 -
1096 +
1736 1097 if (sender === "bot") {
1737 - const lastUserMessage = $chatBox.find('.user-message').last();
1098 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1738 1099 if (lastUserMessage.length) {
1739 - scrollElementToTop(lastUserMessage, botId);
1100 + scrollElementToTop(lastUserMessage);
1740 1101 }
1741 1102 }
1742 -
1743 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1744 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1745 - }
1746 1103 });
1747 1104
1748 1105 if (messageText.id) {
1749 - var instance = MxChatInstances.get(botId);
1750 - instance.lastSeenMessageId = messageText.id;
1751 - hideNotification(botId);
1106 + lastSeenMessageId = messageText.id;
1107 + hideNotification();
1752 1108 }
1753 1109 } catch (error) {
1754 1110 // Error rendering message - silently continue
1755 1111 }
@@ -1755,35 +1111,34 @@
1755 1111 }
1756 1112 }
1757 1113
1758 1114 // Helper function to attach link tracking with proper event handling
1759 -function attachLinkTracking(messageDiv, messageText, botId) {
1760 - botId = botId || 'default';
1115 +function attachLinkTracking(messageDiv, messageText) {
1761 1116 // Use a slight delay to ensure DOM is ready
1762 1117 setTimeout(function() {
1763 1118 const links = messageDiv.find('a[href]').not('[data-tracked]');
1764 -
1119 +
1765 1120 links.each(function() {
1766 1121 const $link = $(this);
1767 1122 const originalHref = $link.attr('href');
1768 -
1123 +
1769 1124 // Mark as tracked to avoid duplicate handlers
1770 1125 $link.attr('data-tracked', 'true');
1771 -
1126 +
1772 1127 // Only track external URLs
1773 1128 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1774 1129 // Remove any existing click handlers first
1775 1130 $link.off('click.tracking');
1776 -
1131 +
1777 1132 // Add new click handler with namespace
1778 1133 $link.on('click.tracking', function(e) {
1779 1134 e.preventDefault();
1780 1135 e.stopPropagation();
1781 -
1782 - const messageContext = typeof messageText === 'string'
1783 - ? messageText.substring(0, 200)
1136 +
1137 + const messageContext = typeof messageText === 'string'
1138 + ? messageText.substring(0, 200)
1784 1139 : '';
1785 -
1140 +
1786 1141 // Track the click
1787 1142 $.ajax({
1788 1143 url: mxchatChat.ajax_url,
1789 1144 type: 'POST',
@@ -1788,9 +1143,9 @@
1788 1143 url: mxchatChat.ajax_url,
1789 1144 type: 'POST',
1790 1145 data: {
1791 1146 action: 'mxchat_track_url_click',
1792 - session_id: getChatSession(botId),
1147 + session_id: getChatSession(),
1793 1148 url: originalHref,
1794 1149 message_context: messageContext,
1795 1150 nonce: mxchatChat.nonce
1796 1151 },
@@ -1802,9 +1157,9 @@
1802 1157 window.location.href = originalHref;
1803 1158 }
1804 1159 }
1805 1160 });
1806 -
1161 +
1807 1162 return false; // Extra insurance to prevent default
1808 1163 });
1809 1164 }
1810 1165 });
@@ -1810,12 +1165,11 @@
1810 1165 });
1811 1166 }, 100); // Small delay to ensure DOM is ready
1812 1167 }
1813 1168
1814 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1169 +function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1815 1170 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1816 - var $chatBox = getElement(botId, 'chat-box');
1817 - var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1171 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1818 1172
1819 1173 // Determine styles
1820 1174 let bgColor, fontColor;
1821 1175 if (sender === "user") {
@@ -1828,13 +1182,27 @@
1828 1182 bgColor = botMessageBgColor;
1829 1183 fontColor = botMessageFontColor;
1830 1184 }
1831 1185
1832 - // Always run linkify to convert markdown links and format text.
1833 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1834 - // to avoid double-processing URLs that are already inside <a> tags).
1835 - var fullMessage = linkify(responseText);
1836 -
1186 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1187 + // This prevents double-processing of URLs that are already formatted as HTML
1188 + var fullMessage;
1189 + if (sender === "user") {
1190 + // Always linkify user messages (they're plain text)
1191 + fullMessage = linkify(responseText);
1192 + } else {
1193 + // For bot/agent messages, check if HTML already exists
1194 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1195 + responseText.includes('<img') || responseText.includes('<div') ||
1196 + responseText.includes('<p>') || responseText.includes('<br>')) {
1197 + // Response already has HTML, don't process it
1198 + fullMessage = responseText;
1199 + } else {
1200 + // Plain text response, apply linkify
1201 + fullMessage = linkify(responseText);
1202 + }
1203 + }
1204 +
1837 1205 if (responseHtml) {
1838 1206 // Only add line breaks if there's actual text content before the HTML
1839 1207 if (fullMessage && fullMessage.trim()) {
1840 1208 fullMessage += '<br><br>' + responseHtml;
@@ -1862,91 +1230,62 @@
1862 1230 lastMessageDiv
1863 1231 .html(fullMessage)
1864 1232 .removeClass('bot-message user-message temporary-message')
1865 1233 .addClass(messageClass)
1866 - .attr('dir', 'auto');
1867 -
1868 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1869 - var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1870 - if (!skipColors) {
1871 - lastMessageDiv.css({
1234 + .attr('dir', 'auto')
1235 + .css({
1872 1236 'background-color': bgColor,
1873 1237 'color': fontColor,
1874 1238 });
1875 - }
1876 1239
1877 1240 // Handle link tracking and scroll
1878 1241 if (sender === "bot" || sender === "agent") {
1879 - attachLinkTracking(lastMessageDiv, responseText, botId);
1242 + attachLinkTracking(lastMessageDiv, responseText);
1880 1243
1881 - const lastUserMessage = $chatBox.find('.user-message').last();
1244 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1882 1245 if (lastUserMessage.length) {
1883 - scrollElementToTop(lastUserMessage, botId);
1246 + scrollElementToTop(lastUserMessage);
1884 1247 }
1885 1248 // Show notification if chat is hidden
1886 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1887 - if ($floatingChatbot.hasClass('hidden')) {
1888 - showNotification(botId);
1249 + if ($('#floating-chatbot').hasClass('hidden')) {
1250 + showNotification();
1889 1251 }
1890 1252 }
1891 1253
1892 1254 // Re-enable chat input after response is displayed
1893 - enableChatInput(botId);
1894 -
1895 - if (sender === "bot" || sender === "agent") {
1896 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1897 - }
1255 + enableChatInput();
1898 1256 } else {
1899 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1257 + appendMessage(sender, responseText, responseHtml, images);
1900 1258 // Re-enable chat input after response is displayed
1901 - enableChatInput(botId);
1259 + enableChatInput();
1902 1260 }
1903 1261 }
1904 1262
1905 1263
1906 - function appendThinkingMessage(botId) {
1907 - botId = botId || 'default';
1264 + function appendThinkingMessage() {
1265 + // Remove any existing thinking dots first
1266 + $('.thinking-dots').remove();
1908 1267
1909 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1910 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1911 - if (indicator && indicator.textContent === 'Live Agent') {
1912 - return;
1913 - }
1914 -
1915 - var $chatBox = getElement(botId, 'chat-box');
1916 -
1917 - // Remove any existing thinking dots in this bot's chat first
1918 - $chatBox.find('.thinking-dots').remove();
1919 -
1920 - // Check if we should skip inline colors (AI theme is active)
1921 - var skipColors = shouldSkipInlineColors(botId);
1922 -
1923 1268 // Retrieve the bot message font color and background color
1924 1269 var botMessageFontColor = mxchatChat.bot_message_font_color;
1925 1270 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1926 1271
1927 - // Build thinking dots HTML - skip inline colors if AI theme is active
1928 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1272 +
1929 1273 var thinkingHtml = '<div class="thinking-dots-container">' +
1930 1274 '<div class="thinking-dots">' +
1931 - '<span class="dot"' + dotStyle + '></span>' +
1932 - '<span class="dot"' + dotStyle + '></span>' +
1933 - '<span class="dot"' + dotStyle + '></span>' +
1275 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1276 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1277 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1934 1278 '</div>' +
1935 1279 '</div>';
1936 1280
1937 - // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1938 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1939 - $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1940 - scrollToBottom(botId);
1281 + // Append the thinking dots to the chat container (or within the temporary message div)
1282 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1283 + scrollToBottom();
1941 1284 }
1942 -
1943 - function removeThinkingDots(botId) {
1944 - botId = botId || 'default';
1945 - var $chatBox = getElement(botId, 'chat-box');
1946 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1947 - $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1948 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1285 +
1286 + function removeThinkingDots() {
1287 + $('.thinking-dots').closest('.temporary-message').remove();
1949 1288 }
1950 1289
1951 1290 // ====================================
1952 1291 // TEXT FORMATTING & PROCESSING
@@ -1980,12 +1319,9 @@
1980 1319 processedText = formatTextStyling(processedText);
1981 1320
1982 1321 // Process code blocks BEFORE processing links
1983 1322 processedText = formatCodeBlocks(processedText);
1984 -
1985 - // Process markdown tables BEFORE converting newlines to paragraphs
1986 - processedText = formatMarkdownTables(processedText);
1987 -
1323 +
1988 1324 // NOW convert to paragraphs
1989 1325 processedText = convertNewlinesToBreaks(processedText);
1990 1326
1991 1327 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1998,63 +1334,37 @@
1998 1334 // Return as a proper link without the brackets
1999 1335 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2000 1336 });
2001 1337
2002 - // Process markdown links: [text](url) and [](url)
2003 - // Uses balanced parenthesis matching to handle URLs containing parens
2004 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2005 - processedText = (function(input) {
2006 - var result = '';
2007 - var i = 0;
2008 - while (i < input.length) {
2009 - // Look for [ at current position
2010 - if (input[i] === '[') {
2011 - // Find closing ]
2012 - var closeBracket = input.indexOf(']', i + 1);
2013 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2014 - result += input[i];
2015 - i++;
2016 - continue;
2017 - }
2018 - var linkText = input.substring(i + 1, closeBracket);
2019 - // Check if URL starts with http
2020 - var urlStart = closeBracket + 2;
2021 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2022 - result += input[i];
2023 - i++;
2024 - continue;
2025 - }
2026 - // Find balanced closing paren
2027 - var depth = 1;
2028 - var j = urlStart;
2029 - while (j < input.length && depth > 0) {
2030 - if (input[j] === '(') depth++;
2031 - else if (input[j] === ')') depth--;
2032 - if (depth > 0) j++;
2033 - }
2034 - if (depth !== 0) {
2035 - result += input[i];
2036 - i++;
2037 - continue;
2038 - }
2039 - var url = input.substring(urlStart, j);
2040 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2041 - var encodedUrl = safeEncodeUrl(cleanUrl);
2042 - if (!linkText || !linkText.trim()) {
2043 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2044 - } else {
2045 - var safeText = sanitizeUserInput(linkText);
2046 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2047 - }
2048 - i = j + 1; // Skip past the closing )
2049 - } else {
2050 - result += input[i];
2051 - i++;
2052 - }
1338 + // Process proper markdown links with text: [text](url)
1339 + // This MUST have non-empty text in the first brackets
1340 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1341 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1342 + // Make sure we have actual text (not just whitespace)
1343 + if (!text || !text.trim()) {
1344 + // If no text, treat the URL as the text
1345 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1346 + const safeUrl = safeEncodeUrl(cleanUrl);
1347 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2053 1348 }
2054 - return result;
2055 - })(processedText);
1349 +
1350 + // Clean the URL
1351 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1352 + const safeUrl = safeEncodeUrl(cleanUrl);
1353 + const safeText = sanitizeUserInput(text);
1354 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1355 + });
2056 1356
1357 + // Handle empty markdown links: [](url)
1358 + // This is a specific case where there's no text
1359 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1360 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1361 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1362 + const safeUrl = safeEncodeUrl(cleanUrl);
1363 + // Use the URL itself as the link text
1364 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1365 + });
1366 +
2057 1367 // Process phone numbers: [text](tel:number)
2058 1368 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2059 1369 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2060 1370 const safePhone = safeEncodeUrl(phone);
@@ -2206,78 +1516,9 @@
2206 1516 });
2207 1517
2208 1518 return text;
2209 1519 }
2210 -
2211 - function formatMarkdownTables(text) {
2212 - var lines = text.split('\n');
2213 - var result = [];
2214 - var i = 0;
2215 -
2216 - while (i < lines.length) {
2217 - // Check for a table: current line has pipes AND next line is a separator row
2218 - if (i + 1 < lines.length &&
2219 - lines[i].indexOf('|') !== -1 &&
2220 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2221 -
2222 - var tableLines = [];
2223 - var headerLine = lines[i];
2224 - var separatorLine = lines[i + 1];
2225 - tableLines.push(headerLine);
2226 - tableLines.push(separatorLine);
2227 -
2228 - // Collect remaining table rows
2229 - var j = i + 2;
2230 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2231 - tableLines.push(lines[j]);
2232 - j++;
2233 - }
2234 -
2235 - // Parse alignment from separator row
2236 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2237 - var alignments = sepCells.map(function(cell) {
2238 - var trimmed = cell.trim();
2239 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2240 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2241 - return 'left';
2242 - });
2243 -
2244 - // Build HTML table
2245 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2246 -
2247 - // Header row
2248 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2249 - html += '<thead><tr>';
2250 - headerCells.forEach(function(cell, idx) {
2251 - var align = alignments[idx] || 'left';
2252 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2253 - });
2254 - html += '</tr></thead>';
2255 -
2256 - // Body rows
2257 - html += '<tbody>';
2258 - for (var r = 2; r < tableLines.length; r++) {
2259 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2260 - html += '<tr>';
2261 - rowCells.forEach(function(cell, idx) {
2262 - var align = alignments[idx] || 'left';
2263 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2264 - });
2265 - html += '</tr>';
2266 - }
2267 - html += '</tbody></table></div>';
2268 -
2269 - result.push(html);
2270 - i = j;
2271 - } else {
2272 - result.push(lines[i]);
2273 - i++;
2274 - }
2275 - }
2276 -
2277 - return result.join('\n');
2278 - }
2279 -
1520 +
2280 1521 function sanitizeUserInput(text) {
2281 1522 const div = document.createElement('div');
2282 1523 div.textContent = text;
2283 1524 return div.innerHTML;
@@ -2307,21 +1548,10 @@
2307 1548 // ====================================
2308 1549 // UI & SCROLLING CONTROLS
2309 1550 // ====================================
2310 1551
2311 - function scrollToBottom(botIdOrInstant, instant) {
2312 - // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
2313 - var botId = 'default';
2314 - if (typeof botIdOrInstant === 'string') {
2315 - botId = botIdOrInstant;
2316 - instant = instant || false;
2317 - } else if (typeof botIdOrInstant === 'boolean') {
2318 - instant = botIdOrInstant;
2319 - } else {
2320 - instant = false;
2321 - }
2322 -
2323 - var chatBox = getElement(botId, 'chat-box');
1552 + function scrollToBottom(instant = false) {
1553 + var chatBox = $('#chat-box');
2324 1554 if (instant) {
2325 1555 // Instantly set the scroll position to the bottom
2326 1556 chatBox.scrollTop(chatBox.prop("scrollHeight"));
2327 1557 } else {
@@ -2330,15 +1560,15 @@
2330 1560 const scrollHeight = chatBox.prop("scrollHeight");
2331 1561 const initialScroll = chatBox.scrollTop();
2332 1562 const distance = scrollHeight - initialScroll;
2333 1563 const duration = 500; // Duration in ms
2334 -
1564 +
2335 1565 function smoothScroll(timestamp) {
2336 1566 if (!start) start = timestamp;
2337 1567 const progress = timestamp - start;
2338 1568 const currentScroll = initialScroll + (distance * (progress / duration));
2339 1569 chatBox.scrollTop(currentScroll);
2340 -
1570 +
2341 1571 if (progress < duration) {
2342 1572 requestAnimationFrame(smoothScroll);
2343 1573 } else {
2344 1574 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -2343,37 +1573,31 @@
2343 1573 } else {
2344 1574 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2345 1575 }
2346 1576 }
2347 -
1577 +
2348 1578 requestAnimationFrame(smoothScroll);
2349 1579 }
2350 1580 }
2351 -
2352 - function scrollElementToTop(element, botId, topOffset) {
2353 - botId = botId || 'default';
2354 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2355 - var chatBox = getElement(botId, 'chat-box');
1581 +
1582 + function scrollElementToTop(element) {
1583 + var chatBox = $('#chat-box');
2356 1584 var elementTop = element.position().top + chatBox.scrollTop();
2357 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1585 + chatBox.animate({ scrollTop: elementTop }, 500);
2358 1586 }
2359 -
2360 - function showChatWidget(botId) {
2361 - botId = botId || 'default';
2362 - var $button = getElement(botId, 'floating-chatbot-button');
1587 +
1588 + function showChatWidget() {
2363 1589 // First ensure display is set
2364 - $button.css('display', 'flex');
1590 + $('#floating-chatbot-button').css('display', 'flex');
2365 1591 // Then handle the fade
2366 - $button.fadeTo(500, 1);
1592 + $('#floating-chatbot-button').fadeTo(500, 1);
2367 1593 // Force visibility
2368 - $button.removeClass('hidden');
1594 + $('#floating-chatbot-button').removeClass('hidden');
2369 1595 }
2370 1596
2371 - function hideChatWidget(botId) {
2372 - botId = botId || 'default';
2373 - var $button = getElement(botId, 'floating-chatbot-button');
2374 - $button.css('display', 'none');
2375 - $button.addClass('hidden');
1597 + function hideChatWidget() {
1598 + $('#floating-chatbot-button').css('display', 'none');
1599 + $('#floating-chatbot-button').addClass('hidden');
2376 1600 }
2377 1601
2378 1602 function disableScroll() {
2379 1603 if (isMobile()) {
@@ -2432,43 +1656,34 @@
2432 1656 chatButton.appendChild(notificationBadge);
2433 1657
2434 1658 }
2435 1659
2436 - function showNotification(botId) {
2437 - botId = botId || 'default';
2438 - const badge = getElementDOM(botId, 'chat-notification-badge');
2439 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2440 - if (badge && $floatingChatbot.hasClass('hidden')) {
1660 + function showNotification() {
1661 + const badge = document.getElementById('chat-notification-badge');
1662 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2441 1663 badge.style.display = 'block';
2442 1664 badge.textContent = '1';
2443 1665 }
2444 1666 }
2445 -
2446 - function hideNotification(botId) {
2447 - botId = botId || 'default';
2448 - const badge = getElementDOM(botId, 'chat-notification-badge');
1667 +
1668 + function hideNotification() {
1669 + const badge = document.getElementById('chat-notification-badge');
2449 1670 if (badge) {
2450 1671 badge.style.display = 'none';
2451 1672 }
2452 1673 }
2453 -
2454 - function startNotificationChecking(botId) {
2455 - botId = botId || 'default';
1674 +
1675 + function startNotificationChecking() {
2456 1676 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2457 1677 if (!chatPersistenceEnabled) return;
2458 -
2459 - createNotificationBadge(botId);
2460 - var instance = MxChatInstances.get(botId);
2461 - instance.notificationCheckInterval = setInterval(function() {
2462 - checkForNewMessages(botId);
2463 - }, 30000); // Check every 30 seconds
1678 +
1679 + createNotificationBadge();
1680 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2464 1681 }
2465 -
2466 - function stopNotificationChecking(botId) {
2467 - botId = botId || 'default';
2468 - var instance = MxChatInstances.get(botId);
2469 - if (instance.notificationCheckInterval) {
2470 - clearInterval(instance.notificationCheckInterval);
1682 +
1683 + function stopNotificationChecking() {
1684 + if (notificationCheckInterval) {
1685 + clearInterval(notificationCheckInterval);
2471 1686 }
2472 1687 }
2473 1688
2474 1689 function checkForNewMessages() {
@@ -2494,35 +1709,28 @@
2494 1709 });
2495 1710 }
2496 1711
2497 1712
2498 -// ====================================
2499 -// LIVE AGENT FUNCTIONALITY
2500 -// ====================================
1713 + // ====================================
1714 + // LIVE AGENT FUNCTIONALITY
1715 + // ====================================
2501 1716
2502 -function startPolling(botId) {
2503 - botId = botId || 'default';
2504 - var instance = MxChatInstances.get(botId);
2505 - // Clear any existing interval first
2506 - stopPolling(botId);
2507 - instance.pollingInterval = setInterval(function() {
2508 - checkForAgentMessages(botId);
2509 - }, 5000);
2510 -}
1717 + function startPolling() {
1718 + // Clear any existing interval first
1719 + stopPolling();
1720 + // Start new polling interval
1721 + pollingInterval = setInterval(checkForAgentMessages, 5000);
1722 + }
2511 1723
2512 -function stopPolling(botId) {
2513 - botId = botId || 'default';
2514 - var instance = MxChatInstances.get(botId);
2515 - if (instance.pollingInterval) {
2516 - clearInterval(instance.pollingInterval);
2517 - instance.pollingInterval = null;
1724 + function stopPolling() {
1725 + if (pollingInterval) {
1726 + clearInterval(pollingInterval);
1727 + pollingInterval = null;
1728 + }
2518 1729 }
2519 -}
2520 -
2521 -function checkForAgentMessages(botId) {
2522 - botId = botId || 'default';
2523 - var instance = MxChatInstances.get(botId);
2524 - const sessionId = getChatSession(botId);
1730 +
1731 +function checkForAgentMessages() {
1732 + const sessionId = getChatSession();
2525 1733 $.ajax({
2526 1734 url: mxchatChat.ajax_url,
2527 1735 type: 'POST',
2528 1736 dataType: 'json',
@@ -2528,41 +1736,32 @@
2528 1736 dataType: 'json',
2529 1737 data: {
2530 1738 action: 'mxchat_fetch_new_messages',
2531 1739 session_id: sessionId,
2532 - last_seen_id: instance.lastSeenMessageId,
2533 - persistence_enabled: 'true',
1740 + last_seen_id: lastSeenMessageId,
1741 + persistence_enabled: 'true', // Add this too
2534 1742 nonce: mxchatChat.nonce
2535 1743 },
2536 1744 success: function (response) {
2537 1745 if (response.success && response.data?.new_messages) {
2538 1746 let hasNewMessage = false;
2539 -
1747 +
2540 1748 response.data.new_messages.forEach(function (message) {
2541 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1749 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2542 1750 hasNewMessage = true;
2543 - appendMessage("agent", message.content, '', [], false, botId);
2544 - instance.lastSeenMessageId = message.id;
2545 - instance.processedMessageIds.add(message.id);
1751 + // CHANGE THIS LINE:
1752 + appendMessage("agent", message.content); // Instead of replaceLastMessage
1753 + lastSeenMessageId = message.id;
1754 + processedMessageIds.add(message.id);
2546 1755 }
2547 1756 });
2548 1757
2549 - if (hasNewMessage) {
2550 - enableChatInput(botId);
1758 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1759 + showNotification();
2551 1760 }
2552 -
2553 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2554 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2555 - showNotification(botId);
2556 - }
2557 -
2558 - scrollToBottom(botId, true);
1761 +
1762 + scrollToBottom(true);
2559 1763 }
2560 -
2561 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2562 - if (response.success && response.data?.chat_mode) {
2563 - updateChatModeIndicator(response.data.chat_mode, botId);
2564 - }
2565 1764 },
2566 1765 error: function (xhr, status, error) {
2567 1766 // Polling error - silently continue
2568 1767 }
@@ -2572,29 +1771,17 @@
2572 1771 // ====================================
2573 1772 // CHAT HISTORY & PERSISTENCE
2574 1773 // ====================================
2575 1774
2576 -function loadChatHistory(botId, onComplete) {
2577 - botId = botId || 'default';
2578 - var instance = MxChatInstances.get(botId);
2579 -
1775 +function loadChatHistory() {
2580 1776 // Prevent duplicate loading
2581 - if (instance.chatHistoryLoaded) {
2582 - if (onComplete) onComplete();
1777 + if (chatHistoryLoaded) {
2583 1778 return;
2584 1779 }
2585 -
2586 - // Use getChatSession which returns null if no session exists (does NOT create one)
2587 - var sessionId = getChatSession(botId);
1780 +
1781 + var sessionId = getChatSession();
2588 1782 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2589 1783
2590 - // No session yet — nothing to load. History will load after first message via ensureSession.
2591 - if (!sessionId) {
2592 - instance.chatHistoryLoaded = true;
2593 - if (onComplete) onComplete();
2594 - return;
2595 - }
2596 -
2597 1784 if (chatPersistenceEnabled && sessionId) {
2598 1785 $.ajax({
2599 1786 url: mxchatChat.ajax_url,
2600 1787 type: 'POST',
@@ -2605,12 +1792,11 @@
2605 1792 },
2606 1793 success: function(response) {
2607 1794 // Handle session reset (IP changed while user was away)
2608 1795 if (response.success === false && response.data && response.data.action === 'reset_session') {
2609 - // Silent reset — new session but don't clear UI
2610 - MxChatInstances.silentResetSession(botId);
2611 - instance.chatHistoryLoaded = true; // Prevent retry loop
2612 - if (onComplete) onComplete();
1796 + // Silently reset session - user will start fresh
1797 + resetChatSession();
1798 + chatHistoryLoaded = true; // Prevent retry loop
2613 1799 return;
2614 1800 }
2615 1801
2616 1802 // Check if the response indicates success
@@ -2616,15 +1802,15 @@
2616 1802 // Check if the response indicates success
2617 1803 if (response.success) {
2618 1804 // Handle case where conversation data exists and is an array
2619 1805 if (response.data && Array.isArray(response.data.conversation)) {
2620 - var $chatBox = getElement(botId, 'chat-box');
1806 + var $chatBox = $('#chat-box');
2621 1807 var $fragment = $(document.createDocumentFragment());
2622 - let highestMessageId = instance.lastSeenMessageId;
1808 + let highestMessageId = lastSeenMessageId;
2623 1809
2624 1810 // Update chat mode if provided
2625 1811 if (response.data.chat_mode) {
2626 - updateChatModeIndicator(response.data.chat_mode, botId);
1812 + updateChatModeIndicator(response.data.chat_mode);
2627 1813 }
2628 1814
2629 1815 // Only process if there are actual messages
2630 1816 if (response.data.conversation.length > 0) {
@@ -2629,9 +1815,9 @@
2629 1815 // Only process if there are actual messages
2630 1816 if (response.data.conversation.length > 0) {
2631 1817 // IMPORTANT: Clear existing messages before loading history
2632 1818 $chatBox.empty();
2633 -
1819 +
2634 1820 $.each(response.data.conversation, function(index, message) {
2635 1821 // Skip agent messages if persistence is off
2636 1822 if (!chatPersistenceEnabled && message.role === 'agent') {
2637 1823 return;
@@ -2666,19 +1852,9 @@
2666 1852 var content = message.content;
2667 1853 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2668 1854 content = decodeHTMLEntities(content);
2669 1855
2670 - // Skip linkify for messages containing structured HTML
2671 - // (forms, product cards, galleries, etc.) to avoid
2672 - // markdown formatting corrupting HTML attributes
2673 - // (e.g. underscores in name="field_name" becoming <em> tags)
2674 - if (content.includes("mxchat-product-card") ||
2675 - content.includes("mxchat-image-gallery") ||
2676 - content.includes("mxchat-featured-products") ||
2677 - content.includes("<form") ||
2678 - content.includes("<input") ||
2679 - content.includes("<select") ||
2680 - content.includes("<textarea")) {
1856 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2681 1857 messageElement.html(content);
2682 1858 } else {
2683 1859 var formattedContent = linkify(content);
2684 1860 messageElement.html(formattedContent);
@@ -2688,47 +1864,42 @@
2688 1864
2689 1865 // Track message IDs
2690 1866 if (message.id) {
2691 1867 highestMessageId = Math.max(highestMessageId, message.id);
2692 - instance.processedMessageIds.add(message.id);
1868 + processedMessageIds.add(message.id);
2693 1869 }
2694 1870 });
2695 1871
2696 1872 // Only append messages and scroll if we have content
2697 1873 $chatBox.append($fragment);
2698 - scrollToBottom(botId, true);
1874 + scrollToBottom(true);
2699 1875
2700 1876 // Collapse quick questions if we have conversation history
2701 - // BUT skip auto-collapse for embedded bots (they should stay expanded)
2702 - if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2703 - collapseQuickQuestions(botId);
1877 + if (hasQuickQuestions()) {
1878 + collapseQuickQuestions();
2704 1879 }
2705 1880
2706 1881 // Update lastSeenMessageId after history loads
2707 - instance.lastSeenMessageId = highestMessageId;
1882 + lastSeenMessageId = highestMessageId;
2708 1883
2709 1884 // Only update chat mode if persistence is enabled and we have messages
2710 1885 if (chatPersistenceEnabled) {
2711 1886 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2712 1887 if (lastMessage.role === 'agent') {
2713 - updateChatModeIndicator('agent', botId);
1888 + updateChatModeIndicator('agent');
2714 1889 }
2715 1890 }
2716 -
1891 +
2717 1892 // Mark as loaded ONLY after successful load
2718 - instance.chatHistoryLoaded = true;
1893 + chatHistoryLoaded = true;
2719 1894 }
2720 1895 }
2721 1896 }
2722 - if (onComplete) onComplete();
2723 1897 },
2724 1898 error: function(xhr, status, error) {
2725 1899 // Error loading chat history - silently continue
2726 - if (onComplete) onComplete();
2727 1900 }
2728 1901 });
2729 - } else {
2730 - if (onComplete) onComplete();
2731 1902 }
2732 1903 }
2733 1904
2734 1905
@@ -2742,12 +1913,11 @@
2742 1913 element.addEventListener(eventType, handler);
2743 1914 }
2744 1915 }
2745 1916
2746 - function showActivePdf(filename, botId) {
2747 - botId = botId || 'default';
2748 - const container = getElementDOM(botId, 'active-pdf-container');
2749 - const nameElement = getElementDOM(botId, 'active-pdf-name');
1917 + function showActivePdf(filename) {
1918 + const container = document.getElementById('active-pdf-container');
1919 + const nameElement = document.getElementById('active-pdf-name');
2750 1920
2751 1921 if (!container || !nameElement) {
2752 1922 return;
2753 1923 }
@@ -2755,12 +1925,11 @@
2755 1925 nameElement.textContent = filename;
2756 1926 container.style.display = 'flex';
2757 1927 }
2758 1928
2759 - function showActiveWord(filename, botId) {
2760 - botId = botId || 'default';
2761 - const container = getElementDOM(botId, 'active-word-container');
2762 - const nameElement = getElementDOM(botId, 'active-word-name');
1929 + function showActiveWord(filename) {
1930 + const container = document.getElementById('active-word-container');
1931 + const nameElement = document.getElementById('active-word-name');
2763 1932
2764 1933 if (!container || !nameElement) {
2765 1934 return;
2766 1935 }
@@ -2767,17 +1936,15 @@
2767 1936
2768 1937 nameElement.textContent = filename;
2769 1938 container.style.display = 'flex';
2770 1939 }
2771 -
2772 - function removeActivePdf(botId) {
2773 - botId = botId || 'default';
2774 - var instance = MxChatInstances.get(botId);
2775 - const container = getElementDOM(botId, 'active-pdf-container');
2776 - const nameElement = getElementDOM(botId, 'active-pdf-name');
2777 -
2778 - if (!container || !nameElement || !instance.activePdfFile) return;
2779 -
1940 +
1941 + function removeActivePdf() {
1942 + const container = document.getElementById('active-pdf-container');
1943 + const nameElement = document.getElementById('active-pdf-name');
1944 +
1945 + if (!container || !nameElement || !activePdfFile) return;
1946 +
2780 1947 fetch(mxchatChat.ajax_url, {
2781 1948 method: 'POST',
2782 1949 headers: {
2783 1950 'Content-Type': 'application/x-www-form-urlencoded',
@@ -2783,9 +1950,9 @@
2783 1950 'Content-Type': 'application/x-www-form-urlencoded',
2784 1951 },
2785 1952 body: new URLSearchParams({
2786 1953 'action': 'mxchat_remove_pdf',
2787 - 'session_id': getChatSession(botId),
1954 + 'session_id': sessionId,
2788 1955 'nonce': mxchatChat.nonce
2789 1956 })
2790 1957 })
2791 1958 .then(response => response.json())
@@ -2836,10 +2003,9 @@
2836 2003 // ====================================
2837 2004 // CONSENT & COMPLIANCE (GDPR)
2838 2005 // ====================================
2839 2006
2840 - function initializeChatVisibility(botId) {
2841 - botId = botId || 'default';
2007 + function initializeChatVisibility() {
2842 2008 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2843 2009 mxchatChat.complianz_toggle === '1' ||
2844 2010 mxchatChat.complianz_toggle === 1;
2845 2011
@@ -2844,35 +2010,34 @@
2844 2010 mxchatChat.complianz_toggle === 1;
2845 2011
2846 2012 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2847 2013 // Initial check
2848 - checkConsentAndShowChat(botId);
2014 + checkConsentAndShowChat();
2849 2015
2850 2016 // Listen for consent changes
2851 2017 $(document).on('cmplz_status_change', function(event) {
2852 - checkConsentAndShowChat(botId);
2018 + checkConsentAndShowChat();
2853 2019 });
2854 2020 } else {
2855 2021 // If Complianz is not enabled, always show
2856 - getElement(botId, 'floating-chatbot-button')
2022 + $('#floating-chatbot-button')
2857 2023 .css('display', 'flex')
2858 2024 .removeClass('hidden no-consent')
2859 2025 .fadeTo(500, 1);
2860 -
2026 +
2861 2027 // Also check pre-chat message when Complianz is not enabled
2862 - checkPreChatDismissal(botId);
2028 + checkPreChatDismissal();
2863 2029 }
2864 2030 }
2865 2031
2866 -
2867 - function checkConsentAndShowChat(botId) {
2868 - botId = botId || 'default';
2032 +
2033 + function checkConsentAndShowChat() {
2869 2034 var consentStatus = cmplz_has_consent('marketing');
2870 2035 var consentType = complianz.consenttype;
2871 2036
2872 - let $widget = getElement(botId, 'floating-chatbot-button');
2873 - let $chatbot = getElement(botId, 'floating-chatbot');
2874 - let $preChat = getElement(botId, 'pre-chat-message');
2037 + let $widget = $('#floating-chatbot-button');
2038 + let $chatbot = $('#floating-chatbot');
2039 + let $preChat = $('#pre-chat-message');
2875 2040
2876 2041 if (consentStatus === true) {
2877 2042 $widget
2878 2043 .removeClass('no-consent')
@@ -2881,9 +2046,9 @@
2881 2046 .fadeTo(500, 1);
2882 2047 $chatbot.removeClass('no-consent');
2883 2048
2884 2049 // Show pre-chat message if not dismissed
2885 - checkPreChatDismissal(botId);
2050 + checkPreChatDismissal();
2886 2051 } else {
2887 2052 $widget
2888 2053 .addClass('no-consent')
2889 2054 .fadeTo(500, 0, function() {
@@ -2901,38 +2066,46 @@
2901 2066
2902 2067 // ====================================
2903 2068 // PRE-CHAT MESSAGE HANDLING
2904 2069 // ====================================
2905 -
2906 - function checkPreChatDismissal(botId) {
2907 - botId = botId || 'default';
2908 - try {
2909 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2910 - if (dismissedAt) {
2911 - // Re-show after 24 hours
2912 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2913 - if (elapsed < 86400000) {
2914 - getElement(botId, 'pre-chat-message').hide();
2915 - return;
2070 +
2071 + function checkPreChatDismissal() {
2072 + $.ajax({
2073 + url: mxchatChat.ajax_url,
2074 + type: 'POST',
2075 + data: {
2076 + action: 'mxchat_check_pre_chat_message_status',
2077 + _ajax_nonce: mxchatChat.nonce
2078 + },
2079 + success: function(response) {
2080 + if (response.success && !response.data.dismissed) {
2081 + $('#pre-chat-message').fadeIn(250);
2082 + } else {
2083 + $('#pre-chat-message').hide();
2916 2084 }
2917 - // Expired — clear and show again
2918 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2085 + },
2086 + error: function() {
2087 + // Error checking pre-chat dismissal - silently continue
2919 2088 }
2920 - getElement(botId, 'pre-chat-message').fadeIn(250);
2921 - } catch (e) {
2922 - // localStorage unavailable — show the message
2923 - getElement(botId, 'pre-chat-message').fadeIn(250);
2924 - }
2089 + });
2925 2090 }
2926 2091
2927 - function handlePreChatDismissal(botId) {
2928 - botId = botId || 'default';
2929 - getElement(botId, 'pre-chat-message').fadeOut(200);
2930 - try {
2931 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2932 - } catch (e) {
2933 - // localStorage unavailable — dismissal won't persist
2934 - }
2092 + function handlePreChatDismissal() {
2093 + $('#pre-chat-message').fadeOut(200);
2094 + $.ajax({
2095 + url: mxchatChat.ajax_url,
2096 + type: 'POST',
2097 + data: {
2098 + action: 'mxchat_dismiss_pre_chat_message',
2099 + _ajax_nonce: mxchatChat.nonce
2100 + },
2101 + success: function() {
2102 + $('#pre-chat-message').hide();
2103 + },
2104 + error: function() {
2105 + // Error dismissing pre-chat message - silently continue
2106 + }
2107 + });
2935 2108 }
2936 2109
2937 2110
2938 2111 // ====================================
@@ -2958,141 +2131,78 @@
2958 2131 // ====================================
2959 2132
2960 2133 $(document).on('click', '.mxchat-popular-question', function () {
2961 2134 var question = $(this).text();
2962 - var botId = getBotIdFromElement(this);
2963 -
2135 +
2964 2136 // Append the question as if the user typed it
2965 - appendMessage("user", question, '', [], false, botId);
2966 -
2137 + appendMessage("user", question);
2138 +
2967 2139 // Only collapse if there are questions
2968 - if (hasQuickQuestions(botId)) {
2969 - collapseQuickQuestions(botId);
2140 + if (hasQuickQuestions()) {
2141 + collapseQuickQuestions();
2970 2142 }
2971 -
2143 +
2972 2144 // Send the question to the server
2973 - sendMessageToChatbot(question, botId);
2145 + sendMessageToChatbot(question);
2974 2146 });
2975 2147
2976 2148 $(document).on('click', '.questions-toggle-btn', function(e) {
2977 2149 e.preventDefault();
2978 2150 e.stopPropagation();
2979 - var botId = getBotIdFromElement(this);
2980 - expandQuickQuestions(botId);
2151 + expandQuickQuestions();
2981 2152 });
2982 2153
2983 2154 $(document).on('click', '.questions-collapse-btn', function(e) {
2984 2155 e.preventDefault();
2985 2156 e.stopPropagation();
2986 - var botId = getBotIdFromElement(this);
2987 - collapseQuickQuestions(botId);
2157 + collapseQuickQuestions();
2988 2158 });
2989 2159
2990 - // Chatbot visibility toggle handlers - use class selector for multi-instance support
2991 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
2992 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
2993 - if (e.type === 'keydown') {
2994 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
2995 - e.preventDefault();
2996 - }
2997 - var botId = getBotIdFromElement(this);
2998 - var $chatbot = getElement(botId, 'floating-chatbot');
2999 - var $badge = getElement(botId, 'chat-notification-badge');
3000 - var $preChat = getElement(botId, 'pre-chat-message');
3001 -
3002 - if ($chatbot.hasClass('hidden')) {
3003 - $chatbot.removeClass('hidden').addClass('visible')
3004 - .attr('aria-modal', 'true').attr('role', 'dialog');
3005 - $(this).addClass('hidden').attr('aria-expanded', 'true');
3006 - $badge.hide(); // Hide notification when opening chat
2160 + // Chatbot visibility toggle handlers
2161 + $(document).on('click', '#floating-chatbot-button', function() {
2162 + var chatbot = $('#floating-chatbot');
2163 + if (chatbot.hasClass('hidden')) {
2164 + chatbot.removeClass('hidden').addClass('visible');
2165 + $(this).addClass('hidden');
2166 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
3007 2167 disableScroll();
3008 - $preChat.fadeOut(250);
3009 -
3010 - // Load chat history for returning visitors (persistence)
3011 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3012 - if (chatPersistenceEnabled) {
3013 - MxChatInstances.ensureSession(botId);
3014 - }
3015 -
3016 - // Deferred email check — only on first widget open
3017 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3018 - var instance = MxChatInstances.get(botId);
3019 - if (emailBlocker && !instance.emailCheckDone) {
3020 - instance.emailCheckDone = true;
3021 - resolveEmailState(botId);
3022 - } else if (!emailBlocker) {
3023 - // No email collection — still route through showChatContainerForBot
3024 - // so the loader is shown while chat history loads
3025 - showChatContainerForBot(botId);
3026 - }
3027 -
3028 - // Move keyboard focus into the message input after the open transition.
3029 - setTimeout(function() {
3030 - var chatInput = getElementDOM(botId, 'chat-input');
3031 - if (chatInput && !chatInput.disabled) {
3032 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3033 - }
3034 - }, 300);
2168 + $('#pre-chat-message').fadeOut(250);
3035 2169 } else {
3036 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3037 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2170 + chatbot.removeClass('visible').addClass('hidden');
2171 + $(this).removeClass('hidden');
3038 2172 enableScroll();
3039 - checkPreChatDismissal(botId);
2173 + checkPreChatDismissal();
3040 2174 }
3041 2175 });
3042 -
3043 - // Allow clicking anywhere on the title bar to close the chatbot.
3044 - // Returns keyboard focus to the launcher so keyboard users don't get
3045 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3046 - // heuristic-based so mouse-triggered close won't show a focus ring.
3047 - $(document).on('click', '.chatbot-top-bar', function() {
3048 - var botId = getBotIdFromElement(this);
3049 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3050 - var $launcher = getElement(botId, 'floating-chatbot-button');
3051 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2176 +
2177 + $(document).on('click', '#exit-chat-button', function() {
2178 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
2179 + $('#floating-chatbot-button').removeClass('hidden');
3052 2180 enableScroll();
3053 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3054 2181 });
3055 -
3056 - // Global Escape-key handler — closes any visible chat widget and
3057 - // returns focus to its launcher. Standard modal-dismissal pattern;
3058 - // pairs with aria-modal="true" set on the widget when it opens.
3059 - $(document).on('keydown', function(e) {
3060 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3061 - var $visible = $('.floating-chatbot.visible');
3062 - if (!$visible.length) return;
3063 - e.preventDefault();
3064 - $visible.each(function() {
3065 - var botId = getBotIdFromElement(this);
3066 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3067 - var $launcher = getElement(botId, 'floating-chatbot-button');
3068 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3069 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3070 - });
3071 - enableScroll();
3072 - });
3073 -
2182 +
3074 2183 $(document).on('click', '.close-pre-chat-message', function(e) {
3075 2184 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3076 - var botId = getBotIdFromElement(this);
3077 - handlePreChatDismissal(botId);
2185 + $('#pre-chat-message').fadeOut(200, function() {
2186 + $(this).remove();
2187 + });
3078 2188 });
2189 +
3079 2190
3080 -
3081 - // PDF upload button handlers - use class selector
3082 - $(document).on('click', '.pdf-upload-btn', function() {
3083 - var botId = getBotIdFromElement(this);
3084 - var pdfInput = getElementDOM(botId, 'pdf-upload');
3085 - if (pdfInput) pdfInput.click();
3086 - });
3087 -
3088 - // Word upload button handlers - use class selector
3089 - $(document).on('click', '.word-upload-btn', function() {
3090 - var botId = getBotIdFromElement(this);
3091 - var wordInput = getElementDOM(botId, 'word-upload');
3092 - if (wordInput) wordInput.click();
3093 - });
2191 + // PDF upload button handlers
2192 + if (document.getElementById('pdf-upload-btn')) {
2193 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2194 + document.getElementById('pdf-upload').click();
2195 + });
2196 + }
3094 2197
2198 + // Word upload button handlers
2199 + if (document.getElementById('word-upload-btn')) {
2200 + document.getElementById('word-upload-btn').addEventListener('click', function() {
2201 + document.getElementById('word-upload').click();
2202 + });
2203 + }
2204 +
3095 2205 // PDF file input change handler
3096 2206 addSafeEventListener('pdf-upload', 'change', async function(e) {
3097 2207 const file = e.target.files[0];
3098 2208
@@ -3116,10 +2226,8 @@
3116 2226 const sendBtn = document.getElementById('send-button');
3117 2227 const originalBtnContent = uploadBtn.innerHTML;
3118 2228
3119 2229 try {
3120 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3121 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3122 2230 const formData = new FormData();
3123 2231 formData.append('action', 'mxchat_upload_pdf');
3124 2232 formData.append('pdf_file', file);
3125 2233 formData.append('session_id', sessionId);
@@ -3183,10 +2291,8 @@
3183 2291 const sendBtn = document.getElementById('send-button');
3184 2292 const originalBtnContent = uploadBtn.innerHTML;
3185 2293
3186 2294 try {
3187 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3188 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3189 2295 const formData = new FormData();
3190 2296 formData.append('action', 'mxchat_upload_word');
3191 2297 formData.append('word_file', file);
3192 2298 formData.append('session_id', sessionId);
@@ -3280,497 +2386,431 @@
3280 2386 });
3281 2387
3282 2388
3283 2389 // ====================================
3284 -// INIT LOADER & CHAT CONTAINER HELPERS
2390 +// EMAIL COLLECTION SETUP - FIXED VERSION
3285 2391 // ====================================
3286 -// These must be outside the email collection block so they're always available
3287 -// (used by persistence loading even when email collection is off)
3288 -
3289 -function showInitLoader(botId) {
3290 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3291 - if (loader) loader.style.display = 'flex';
3292 -}
3293 -
3294 -function hideInitLoader(botId) {
3295 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3296 - if (loader) loader.style.display = 'none';
3297 -}
3298 -
3299 -function showEmailFormForBot(botId) {
3300 - hideInitLoader(botId);
3301 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3302 - var chatContainer = getElementDOM(botId, 'chat-container');
3303 - if (emailBlocker) emailBlocker.style.display = 'flex';
3304 - if (chatContainer) chatContainer.style.display = 'none';
3305 -}
3306 -
3307 -function showChatContainerForBot(botId) {
3308 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3309 - var chatContainer = getElementDOM(botId, 'chat-container');
3310 - if (emailBlocker) emailBlocker.style.display = 'none';
3311 -
3312 - var instance = MxChatInstances.get(botId);
3313 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3314 -
3315 - // If persistence is on and history hasn't loaded yet, show loader
3316 - // while history loads to prevent flash of empty chat
3317 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3318 - if (chatContainer) chatContainer.style.display = 'none';
3319 - showInitLoader(botId);
3320 - loadChatHistory(botId, function() {
3321 - hideInitLoader(botId);
3322 - if (chatContainer) chatContainer.style.display = 'flex';
3323 - scrollToBottom(botId, true);
3324 - });
3325 - } else {
3326 - hideInitLoader(botId);
3327 - if (chatContainer) chatContainer.style.display = 'flex';
3328 - if (typeof loadChatHistory === 'function') {
3329 - loadChatHistory(botId);
3330 - }
3331 - }
3332 -}
3333 -
3334 -// ====================================
3335 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3336 -// ====================================
3337 2392 // Only run email collection setup if it's enabled
3338 2393 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2394 + // Email collection form setup and handlers
2395 + const emailForm = document.getElementById('email-collection-form');
2396 + const emailBlocker = document.getElementById('email-blocker');
2397 + const chatbotWrapper = document.getElementById('chat-container');
3339 2398
3340 - // Track submitting state per bot
3341 - const emailSubmittingState = {};
2399 + if (emailForm && emailBlocker && chatbotWrapper) {
2400 +
2401 + // Add loading state management
2402 + let isSubmitting = false;
2403 +
2404 + // Optimized UI transition functions
2405 + function showEmailForm() {
2406 + emailBlocker.style.display = 'flex';
2407 + chatbotWrapper.style.display = 'none';
2408 + }
3342 2409
3343 - // Add CSS animations for email form (once globally)
3344 - if (!document.getElementById('email-error-styles')) {
3345 - const style = document.createElement('style');
3346 - style.id = 'email-error-styles';
3347 - style.textContent = `
3348 - @keyframes fadeInError {
3349 - from { opacity: 0; transform: translateY(-5px); }
3350 - to { opacity: 1; transform: translateY(0); }
2410 + function showChatContainer() {
2411 + // Show chat immediately without delay
2412 + emailBlocker.style.display = 'none';
2413 + chatbotWrapper.style.display = 'flex';
2414 +
2415 + // Load chat history only after showing chat container
2416 + if (typeof loadChatHistory === 'function') {
2417 + loadChatHistory();
3351 2418 }
3352 - .email-input-shake {
3353 - animation: shake 0.5s ease-in-out;
3354 - }
3355 - @keyframes shake {
3356 - 0%, 100% { transform: translateX(0); }
3357 - 25% { transform: translateX(-5px); }
3358 - 75% { transform: translateX(5px); }
3359 - }
3360 - @keyframes spin {
3361 - from { transform: rotate(0deg); }
3362 - to { transform: rotate(360deg); }
3363 - }
3364 - .email-spinner {
3365 - display: inline-block;
3366 - vertical-align: middle;
3367 - }
3368 - `;
3369 - document.head.appendChild(style);
3370 - }
2419 + }
3371 2420
3372 - function isValidEmailAddress(email) {
3373 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3374 - return emailRegex.test(email.trim()) && email.length <= 254;
3375 - }
2421 + // Enhanced email validation
2422 + function isValidEmail(email) {
2423 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2424 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2425 + }
3376 2426
3377 - function isValidNameInput(name) {
3378 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3379 - }
2427 + // Enhanced name validation
2428 + function isValidName(name) {
2429 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2430 + }
3380 2431
3381 - /**
3382 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3383 - * @param {string} botId - The bot instance ID
3384 - * @param {string} visitorName - The visitor's name to insert
3385 - */
3386 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3387 - var chatBox = getElementDOM(botId, 'chat-box');
3388 - if (!chatBox) return;
3389 -
3390 - // Find the first bot message (intro message)
3391 - var introMessage = chatBox.querySelector('.bot-message');
3392 - if (!introMessage) return;
3393 -
3394 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3395 - if (!messageContent) return;
3396 -
3397 - var html = messageContent.innerHTML;
3398 -
3399 - // Replace {visitor_name} placeholder (case-insensitive)
3400 - if (visitorName && visitorName.trim()) {
3401 - // Escape HTML to prevent XSS
3402 - var safeName = $('<div>').text(visitorName.trim()).html();
3403 - html = html.replace(/\{visitor_name\}/gi, safeName);
3404 - } else {
3405 - // Remove placeholder and clean up spacing if no name provided
3406 - html = html.replace(/\{visitor_name\}/gi, '');
3407 - // Clean up any double spaces that might result
3408 - html = html.replace(/\s{2,}/g, ' ').trim();
2432 + // Show loading state with spinner
2433 + function setSubmissionState(loading) {
2434 + const submitButton = document.getElementById('email-submit-button');
2435 + const emailInput = document.getElementById('user-email');
2436 + const nameInput = document.getElementById('user-name');
2437 +
2438 + if (loading) {
2439 + isSubmitting = true;
2440 + if (submitButton) submitButton.disabled = true;
2441 + if (emailInput) emailInput.disabled = true;
2442 + if (nameInput) nameInput.disabled = true;
2443 +
2444 + // Store original content and add spinner
2445 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2446 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2447 +
2448 + // Add loading spinner while keeping original text
2449 + const originalText = submitButton.textContent;
2450 + submitButton.innerHTML = `
2451 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2452 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2453 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2454 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2455 + </circle>
2456 + </svg>
2457 + ${originalText}
2458 + `;
2459 +
2460 + submitButton.style.opacity = '0.8';
2461 + }
2462 + } else {
2463 + isSubmitting = false;
2464 + if (submitButton) submitButton.disabled = false;
2465 + if (emailInput) emailInput.disabled = false;
2466 + if (nameInput) nameInput.disabled = false;
2467 +
2468 + // Restore original content
2469 + if (submitButton) {
2470 + const originalHtml = submitButton.getAttribute('data-original-html');
2471 + if (originalHtml) {
2472 + submitButton.innerHTML = originalHtml;
2473 + }
2474 + submitButton.style.opacity = '1';
2475 + }
2476 + }
3409 2477 }
3410 2478
3411 - messageContent.innerHTML = html;
3412 - }
3413 -
3414 - function setEmailSubmissionState(botId, loading) {
3415 - var submitButton = getElementDOM(botId, 'email-submit-button');
3416 - var emailInput = getElementDOM(botId, 'user-email');
3417 - var nameInput = getElementDOM(botId, 'user-name');
3418 -
3419 - if (loading) {
3420 - emailSubmittingState[botId] = true;
3421 - if (submitButton) submitButton.disabled = true;
3422 - if (emailInput) emailInput.disabled = true;
3423 - if (nameInput) nameInput.disabled = true;
3424 -
3425 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3426 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3427 - const originalText = submitButton.textContent;
3428 - submitButton.innerHTML = `
3429 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3430 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3431 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3432 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3433 - </circle>
3434 - </svg>
3435 - ${originalText}
2479 + // Error display functions
2480 + function showEmailError(message) {
2481 + clearEmailError();
2482 +
2483 + const errorDiv = document.createElement('div');
2484 + errorDiv.className = 'email-error';
2485 + errorDiv.style.cssText = `
2486 + color: #e74c3c;
2487 + font-size: 12px;
2488 + margin-top: 8px;
2489 + padding: 4px 0;
2490 + animation: fadeInError 0.3s ease;
2491 + `;
2492 + errorDiv.textContent = message;
2493 +
2494 + // Add CSS animation if not already present
2495 + if (!document.getElementById('email-error-styles')) {
2496 + const style = document.createElement('style');
2497 + style.id = 'email-error-styles';
2498 + style.textContent = `
2499 + @keyframes fadeInError {
2500 + from { opacity: 0; transform: translateY(-5px); }
2501 + to { opacity: 1; transform: translateY(0); }
2502 + }
2503 + .email-input-shake {
2504 + animation: shake 0.5s ease-in-out;
2505 + }
2506 + @keyframes shake {
2507 + 0%, 100% { transform: translateX(0); }
2508 + 25% { transform: translateX(-5px); }
2509 + 75% { transform: translateX(5px); }
2510 + }
2511 + @keyframes spin {
2512 + from { transform: rotate(0deg); }
2513 + to { transform: rotate(360deg); }
2514 + }
2515 + .email-spinner {
2516 + display: inline-block;
2517 + vertical-align: middle;
2518 + }
3436 2519 `;
3437 - submitButton.style.opacity = '0.8';
2520 + document.head.appendChild(style);
3438 2521 }
3439 - } else {
3440 - emailSubmittingState[botId] = false;
3441 - if (submitButton) submitButton.disabled = false;
3442 - if (emailInput) emailInput.disabled = false;
3443 - if (nameInput) nameInput.disabled = false;
3444 -
3445 - if (submitButton) {
3446 - const originalHtml = submitButton.getAttribute('data-original-html');
3447 - if (originalHtml) {
3448 - submitButton.innerHTML = originalHtml;
3449 - }
3450 - submitButton.style.opacity = '1';
2522 +
2523 + emailForm.appendChild(errorDiv);
2524 +
2525 + // Add shake animation to inputs
2526 + const emailInput = document.getElementById('user-email');
2527 + const nameInput = document.getElementById('user-name');
2528 +
2529 + if (emailInput) {
2530 + emailInput.classList.add('email-input-shake');
2531 + setTimeout(() => {
2532 + emailInput.classList.remove('email-input-shake');
2533 + }, 500);
3451 2534 }
2535 +
2536 + if (nameInput) {
2537 + nameInput.classList.add('email-input-shake');
2538 + setTimeout(() => {
2539 + nameInput.classList.remove('email-input-shake');
2540 + }, 500);
2541 + }
3452 2542 }
3453 - }
3454 2543
3455 - function showEmailError(botId, message) {
3456 - clearEmailError(botId);
3457 -
3458 - var emailForm = getElementDOM(botId, 'email-collection-form');
3459 - if (!emailForm) return;
3460 -
3461 - const errorDiv = document.createElement('div');
3462 - errorDiv.className = 'email-error';
3463 - errorDiv.style.cssText = `
3464 - color: #e74c3c;
3465 - font-size: 12px;
3466 - margin-top: 8px;
3467 - padding: 4px 0;
3468 - animation: fadeInError 0.3s ease;
3469 - `;
3470 - errorDiv.textContent = message;
3471 - emailForm.appendChild(errorDiv);
3472 -
3473 - // Add shake animation to inputs
3474 - var emailInput = getElementDOM(botId, 'user-email');
3475 - var nameInput = getElementDOM(botId, 'user-name');
3476 -
3477 - if (emailInput) {
3478 - emailInput.classList.add('email-input-shake');
3479 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3480 - }
3481 - if (nameInput) {
3482 - nameInput.classList.add('email-input-shake');
3483 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3484 - }
3485 - }
3486 -
3487 - function clearEmailError(botId) {
3488 - var emailForm = getElementDOM(botId, 'email-collection-form');
3489 - if (emailForm) {
2544 + function clearEmailError() {
3490 2545 const existingErrors = emailForm.querySelectorAll('.email-error');
3491 2546 existingErrors.forEach(error => error.remove());
3492 2547 }
3493 - }
3494 2548
3495 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3496 - function resolveEmailState(botId) {
3497 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3498 - if (mxchatChat.initial_email_state.show_email_form) {
3499 - showEmailFormForBot(botId);
3500 - } else {
3501 - showChatContainerForBot(botId);
3502 - }
3503 - } else {
3504 - checkSessionAndEmailForBot(botId);
3505 - }
3506 - }
2549 + // MAIN FORM SUBMIT HANDLER
2550 + // Remove any existing event listeners first
2551 + emailForm.removeEventListener('submit', handleFormSubmit);
3507 2552
3508 - function checkSessionAndEmailForBot(botId) {
3509 - const sessionId = MxChatInstances.ensureSession(botId);
2553 + // Add the form submit handler
2554 + emailForm.addEventListener('submit', handleFormSubmit);
3510 2555
3511 - // Hide both panels while we check — show loader instead
3512 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3513 - var chatContainer = getElementDOM(botId, 'chat-container');
3514 - if (emailBlocker) emailBlocker.style.display = 'none';
3515 - if (chatContainer) chatContainer.style.display = 'none';
3516 - showInitLoader(botId);
2556 + function handleFormSubmit(event) {
2557 + event.preventDefault();
2558 + event.stopPropagation();
3517 2559
3518 - fetch(mxchatChat.ajax_url, {
3519 - method: 'POST',
3520 - headers: {
3521 - 'Content-Type': 'application/x-www-form-urlencoded',
3522 - },
3523 - body: new URLSearchParams({
3524 - action: 'mxchat_check_email_provided',
3525 - session_id: sessionId,
3526 - nonce: mxchatChat.nonce,
3527 - })
3528 - })
3529 - .then((response) => {
3530 - if (!response.ok) {
3531 - throw new Error(`HTTP error! status: ${response.status}`);
2560 + // Prevent double submission
2561 + if (isSubmitting) {
2562 + return false;
3532 2563 }
3533 - return response.json();
3534 - })
3535 - .then((data) => {
3536 - if (data.success) {
3537 - if (data.data.logged_in || data.data.email) {
3538 - showChatContainerForBot(botId);
3539 - } else {
3540 - showEmailFormForBot(botId);
3541 - }
3542 - } else {
3543 - showEmailFormForBot(botId);
3544 - }
3545 - })
3546 - .catch((error) => {
3547 - showEmailFormForBot(botId);
3548 - });
3549 - }
3550 2564
3551 - // Event delegation for email form submission
3552 - $(document).on('submit', '.email-collection-form', function(e) {
3553 - e.preventDefault();
3554 - e.stopPropagation();
2565 + const userEmail = document.getElementById('user-email').value.trim();
2566 + const nameInput = document.getElementById('user-name');
2567 + const userName = nameInput ? nameInput.value.trim() : '';
2568 + const sessionId = getChatSession();
3555 2569
3556 - var botId = getBotIdFromElement(this);
2570 + // Validate email before submission
2571 + if (!userEmail) {
2572 + showEmailError('Please enter your email address.');
2573 + return false;
2574 + }
3557 2575
3558 - // Prevent double submission
3559 - if (emailSubmittingState[botId]) {
3560 - return false;
3561 - }
2576 + if (!isValidEmail(userEmail)) {
2577 + showEmailError('Please enter a valid email address.');
2578 + return false;
2579 + }
3562 2580
3563 - var emailInput = getElementDOM(botId, 'user-email');
3564 - var nameInput = getElementDOM(botId, 'user-name');
3565 - var userEmail = emailInput ? emailInput.value.trim() : '';
3566 - var userName = nameInput ? nameInput.value.trim() : '';
3567 - var sessionId = MxChatInstances.ensureSession(botId);
2581 + // Validate name if field exists
2582 + if (nameInput && !isValidName(userName)) {
2583 + showEmailError('Please enter a valid name (2-100 characters).');
2584 + return false;
2585 + }
3568 2586
3569 - // Validate email
3570 - if (!userEmail) {
3571 - showEmailError(botId, 'Please enter your email address.');
3572 - return false;
3573 - }
2587 + // Clear any existing errors
2588 + clearEmailError();
2589 + setSubmissionState(true);
3574 2590
3575 - if (!isValidEmailAddress(userEmail)) {
3576 - showEmailError(botId, 'Please enter a valid email address.');
3577 - return false;
3578 - }
2591 + // Prepare form data with optional name
2592 + const formData = new URLSearchParams({
2593 + action: 'mxchat_handle_save_email_and_response',
2594 + email: userEmail,
2595 + session_id: sessionId,
2596 + nonce: mxchatChat.nonce,
2597 + });
3579 2598
3580 - // Validate name if field exists and has content
3581 - if (nameInput && userName && !isValidNameInput(userName)) {
3582 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3583 - return false;
3584 - }
2599 + // Add name to form data if provided
2600 + if (userName) {
2601 + formData.append('name', userName);
2602 + }
3585 2603
3586 - clearEmailError(botId);
3587 - setEmailSubmissionState(botId, true);
2604 + fetch(mxchatChat.ajax_url, {
2605 + method: 'POST',
2606 + headers: {
2607 + 'Content-Type': 'application/x-www-form-urlencoded',
2608 + },
2609 + body: formData
2610 + })
2611 + .then((response) => {
2612 + if (!response.ok) {
2613 + throw new Error(`HTTP error! status: ${response.status}`);
2614 + }
2615 + return response.json();
2616 + })
2617 + .then((data) => {
2618 + setSubmissionState(false);
3588 2619
3589 - // Prepare form data
3590 - const formData = new URLSearchParams({
3591 - action: 'mxchat_handle_save_email_and_response',
3592 - email: userEmail,
3593 - session_id: sessionId,
3594 - nonce: mxchatChat.nonce,
3595 - });
2620 + if (data.success) {
2621 + // Show chat immediately
2622 + showChatContainer();
3596 2623
3597 - if (userName) {
3598 - formData.append('name', userName);
2624 + // Handle bot response if provided
2625 + if (data.message && typeof appendMessage === 'function') {
2626 + setTimeout(() => {
2627 + appendMessage('bot', data.message);
2628 + if (typeof scrollToBottom === 'function') {
2629 + scrollToBottom();
2630 + }
2631 + }, 100);
2632 + }
2633 + } else {
2634 + showEmailError(data.message || 'Failed to save email. Please try again.');
2635 + }
2636 + })
2637 + .catch((error) => {
2638 + setSubmissionState(false);
2639 + showEmailError('An error occurred. Please try again.');
2640 + });
2641 +
2642 + return false; // Extra prevention
3599 2643 }
3600 2644
3601 - fetch(mxchatChat.ajax_url, {
3602 - method: 'POST',
3603 - headers: {
3604 - 'Content-Type': 'application/x-www-form-urlencoded',
3605 - },
3606 - body: formData
3607 - })
3608 - .then((response) => {
3609 - if (!response.ok) {
3610 - throw new Error(`HTTP error! status: ${response.status}`);
3611 - }
3612 - return response.json();
3613 - })
3614 - .then((data) => {
3615 - setEmailSubmissionState(botId, false);
2645 + // Real-time email validation
2646 + const emailInput = document.getElementById('user-email');
2647 + if (emailInput) {
2648 + let validationTimeout;
2649 +
2650 + emailInput.addEventListener('input', function() {
2651 + // Clear previous validation timeout
2652 + if (validationTimeout) {
2653 + clearTimeout(validationTimeout);
2654 + }
2655 +
2656 + // Debounce validation
2657 + validationTimeout = setTimeout(() => {
2658 + const email = this.value.trim();
2659 + clearEmailError();
2660 +
2661 + if (email && !isValidEmail(email)) {
2662 + showEmailError('Please enter a valid email address.');
2663 + }
2664 + }, 500);
2665 + });
3616 2666
3617 - if (data.success) {
3618 - showChatContainerForBot(botId);
2667 + // Handle Enter key
2668 + emailInput.addEventListener('keypress', function(e) {
2669 + if (e.key === 'Enter' && !isSubmitting) {
2670 + e.preventDefault();
2671 + emailForm.dispatchEvent(new Event('submit'));
2672 + }
2673 + });
2674 + }
3619 2675
3620 - // Replace {visitor_name} placeholder in intro message with actual name
3621 - if (userName) {
3622 - replaceVisitorNamePlaceholder(botId, userName);
3623 - } else {
3624 - // Remove placeholder if no name provided
3625 - replaceVisitorNamePlaceholder(botId, '');
2676 + // Real-time name validation
2677 + const nameInput = document.getElementById('user-name');
2678 + if (nameInput) {
2679 + let nameValidationTimeout;
2680 +
2681 + nameInput.addEventListener('input', function() {
2682 + // Clear previous validation timeout
2683 + if (nameValidationTimeout) {
2684 + clearTimeout(nameValidationTimeout);
3626 2685 }
2686 +
2687 + // Debounce validation
2688 + nameValidationTimeout = setTimeout(() => {
2689 + const name = this.value.trim();
2690 + clearEmailError();
2691 +
2692 + if (name && !isValidName(name)) {
2693 + showEmailError('Name must be between 2 and 100 characters.');
2694 + }
2695 + }, 500);
2696 + });
3627 2697
3628 - if (data.message && typeof appendMessage === 'function') {
3629 - setTimeout(() => {
3630 - appendMessage('bot', data.message, '', [], false, botId);
3631 - if (typeof scrollToBottom === 'function') {
3632 - scrollToBottom(botId);
3633 - }
3634 - }, 100);
2698 + // Handle Enter key
2699 + nameInput.addEventListener('keypress', function(e) {
2700 + if (e.key === 'Enter' && !isSubmitting) {
2701 + e.preventDefault();
2702 + emailForm.dispatchEvent(new Event('submit'));
3635 2703 }
2704 + });
2705 + }
2706 +
2707 + // Initial state check
2708 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2709 + const emailState = mxchatChat.initial_email_state;
2710 + if (emailState.show_email_form) {
2711 + showEmailForm();
3636 2712 } else {
3637 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2713 + showChatContainer();
3638 2714 }
3639 - })
3640 - .catch((error) => {
3641 - setEmailSubmissionState(botId, false);
3642 - showEmailError(botId, 'An error occurred. Please try again.');
3643 - });
3644 -
3645 - return false;
3646 - });
3647 -
3648 - // Real-time email validation using event delegation
3649 - $(document).on('input', '.mxchat-email-input', function() {
3650 - var botId = getBotIdFromElement(this);
3651 - var $input = $(this);
3652 -
3653 - // Clear previous timeout
3654 - clearTimeout($input.data('validationTimeout'));
3655 -
3656 - // Debounce validation
3657 - var timeout = setTimeout(() => {
3658 - var email = this.value.trim();
3659 - clearEmailError(botId);
3660 -
3661 - if (email && !isValidEmailAddress(email)) {
3662 - showEmailError(botId, 'Please enter a valid email address.');
3663 - }
3664 - }, 500);
3665 -
3666 - $input.data('validationTimeout', timeout);
3667 - });
3668 -
3669 - // Handle Enter key in email input
3670 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3671 - if (e.key === 'Enter') {
3672 - e.preventDefault();
3673 - var botId = getBotIdFromElement(this);
3674 - if (!emailSubmittingState[botId]) {
3675 - $(this).closest('.email-collection-form').submit();
3676 - }
2715 + } else {
2716 + // Check email status via AJAX
2717 + setTimeout(checkSessionAndEmail, 100);
3677 2718 }
3678 - });
3679 2719
3680 - // Handle Enter key in name input
3681 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3682 - if (e.key === 'Enter') {
3683 - e.preventDefault();
3684 - var botId = getBotIdFromElement(this);
3685 - if (!emailSubmittingState[botId]) {
3686 - $(this).closest('.email-collection-form').submit();
3687 - }
2720 + // Check if email exists for the current session
2721 + function checkSessionAndEmail() {
2722 + const sessionId = getChatSession();
2723 +
2724 + fetch(mxchatChat.ajax_url, {
2725 + method: 'POST',
2726 + headers: {
2727 + 'Content-Type': 'application/x-www-form-urlencoded',
2728 + },
2729 + body: new URLSearchParams({
2730 + action: 'mxchat_check_email_provided',
2731 + session_id: sessionId,
2732 + nonce: mxchatChat.nonce,
2733 + })
2734 + })
2735 + .then((response) => {
2736 + if (!response.ok) {
2737 + throw new Error(`HTTP error! status: ${response.status}`);
2738 + }
2739 + return response.json();
2740 + })
2741 + .then((data) => {
2742 + if (data.success) {
2743 + if (data.data.logged_in || data.data.email) {
2744 + showChatContainer();
2745 + } else {
2746 + showEmailForm();
2747 + }
2748 + } else {
2749 + // On error, default to showing email form
2750 + showEmailForm();
2751 + }
2752 + })
2753 + .catch((error) => {
2754 + // Email check failed - default to email form
2755 + showEmailForm();
2756 + });
3688 2757 }
3689 - });
3690 2758
3691 - // Initialize email check for all bot instances
3692 - // For floating bots: defer until widget is opened (zero passive AJAX)
3693 - // For embedded bots: check immediately since the form is visible
3694 - $('.mxchat-chatbot-wrapper').each(function() {
3695 - var botId = $(this).data('bot-id') || 'default';
3696 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3697 -
3698 - if (emailBlocker) {
3699 - if (isEmbeddedBot(botId)) {
3700 - // Embedded bots are always visible — check now
3701 - resolveEmailState(botId);
3702 - }
3703 - // Floating bots: handled in the widget open handler
3704 - } else if (isEmbeddedBot(botId)) {
3705 - // Embedded bot, no email collection — load history with loader
3706 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3707 - if (chatPersistenceEnabled) {
3708 - MxChatInstances.ensureSession(botId);
3709 - showChatContainerForBot(botId);
3710 - }
3711 - }
3712 - });
2759 + } else {
2760 + // Email collection is enabled but essential elements are missing - silently continue
2761 + }
3713 2762 }
3714 2763
3715 - // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3716 - $(document).on('click', '.pre-chat-message', function() {
3717 - var botId = getBotIdFromElement(this);
3718 - var $chatbot = getElement(botId, 'floating-chatbot');
3719 - if ($chatbot.hasClass('hidden')) {
3720 - $chatbot.removeClass('hidden').addClass('visible');
3721 - getElement(botId, 'floating-chatbot-button').addClass('hidden');
3722 - handlePreChatDismissal(botId);
2764 + // Open chatbot when pre-chat message is clicked
2765 + $(document).on('click', '#pre-chat-message', function() {
2766 + var chatbot = $('#floating-chatbot');
2767 + if (chatbot.hasClass('hidden')) {
2768 + chatbot.removeClass('hidden').addClass('visible');
2769 + $('#floating-chatbot-button').addClass('hidden');
2770 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
3723 2771 disableScroll(); // Disable scroll when chatbot opens
3724 -
3725 - // Load chat history for returning visitors (persistence)
3726 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3727 - if (chatPersistenceEnabled) {
3728 - MxChatInstances.ensureSession(botId);
3729 - }
3730 -
3731 - // Deferred email check — only on first widget open
3732 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3733 - var instance = MxChatInstances.get(botId);
3734 - if (emailBlocker && !instance.emailCheckDone) {
3735 - instance.emailCheckDone = true;
3736 - resolveEmailState(botId);
3737 - } else if (!emailBlocker) {
3738 - showChatContainerForBot(botId);
3739 - }
3740 2772 }
3741 2773 });
3742 2774
3743 - // Legacy duplicate close handler removed — handled by single event delegation above
2775 + var closeButton = document.querySelector('.close-pre-chat-message');
2776 + if (closeButton) {
2777 + closeButton.addEventListener('click', function() {
2778 + $('#pre-chat-message').fadeOut(200); // Hide the message
3744 2779
3745 -
3746 -function hasQuickQuestions(botId) {
3747 - botId = botId || 'default';
3748 - var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3749 - if (!questionsContainer) return false;
3750 - const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
2780 + // Send an AJAX request to set the transient flag for 24 hours
2781 + $.ajax({
2782 + url: mxchatChat.ajax_url,
2783 + type: 'POST',
2784 + data: {
2785 + action: 'mxchat_dismiss_pre_chat_message',
2786 + _ajax_nonce: mxchatChat.nonce
2787 + },
2788 + success: function() {
2789 + // Ensure the message is hidden after dismissal
2790 + $('#pre-chat-message').hide();
2791 + },
2792 + error: function() {
2793 + // Error dismissing pre-chat message - silently continue
2794 + }
2795 + });
2796 + });
2797 + }
2798 +
2799 +
2800 +function hasQuickQuestions() {
2801 + const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
3751 2802 return questionButtons.length > 0;
3752 2803 }
3753 2804
3754 -/**
3755 - * Check if a bot is embedded (not floating)
3756 - * Embedded bots don't have a .floating-chatbot wrapper
3757 - */
3758 -function isEmbeddedBot(botId) {
3759 - botId = botId || 'default';
3760 - var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3761 - return !floatingWrapper;
3762 -}
3763 -
3764 -function collapseQuickQuestions(botId) {
3765 - botId = botId || 'default';
3766 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3767 - if (questionsContainer && hasQuickQuestions(botId)) {
2805 +function collapseQuickQuestions() {
2806 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2807 + if (questionsContainer && hasQuickQuestions()) {
3768 2808 questionsContainer.classList.add('collapsed');
3769 2809 questionsContainer.classList.add('has-been-collapsed');
3770 2810 try {
3771 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3772 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
2811 + sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2812 + sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
3773 2813 } catch (e) {
3774 2814 // Ignore if sessionStorage is not available
3775 2815 }
3776 2816 }
@@ -3775,15 +2815,14 @@
3775 2815 }
3776 2816 }
3777 2817 }
3778 2818
3779 -function expandQuickQuestions(botId) {
3780 - botId = botId || 'default';
3781 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3782 - if (questionsContainer && hasQuickQuestions(botId)) {
2819 +function expandQuickQuestions() {
2820 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2821 + if (questionsContainer && hasQuickQuestions()) {
3783 2822 questionsContainer.classList.remove('collapsed');
3784 2823 try {
3785 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
2824 + sessionStorage.setItem('mxchat_questions_collapsed', 'false');
3786 2825 } catch (e) {
3787 2826 // Ignore if sessionStorage is not available
3788 2827 }
3789 2828 }
@@ -3788,24 +2827,18 @@
3788 2827 }
3789 2828 }
3790 2829 }
3791 2830
3792 -function checkQuickQuestionsState(botId) {
3793 - botId = botId || 'default';
3794 - if (!hasQuickQuestions(botId)) {
2831 +function checkQuickQuestionsState() {
2832 + if (!hasQuickQuestions()) {
3795 2833 return; // Don't do anything if no questions exist
3796 2834 }
3797 -
3798 - // Skip restoring collapsed state for embedded bots - they should always start expanded
3799 - if (isEmbeddedBot(botId)) {
3800 - return;
3801 - }
3802 -
2835 +
3803 2836 try {
3804 - const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3805 - const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3806 -
3807 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
2837 + const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2838 + const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2839 +
2840 + const questionsContainer = document.getElementById('mxchat-popular-questions');
3808 2841 if (questionsContainer) {
3809 2842 if (hasBeenCollapsed === 'true') {
3810 2843 questionsContainer.classList.add('has-been-collapsed');
3811 2844 }
@@ -3818,36 +2851,32 @@
3818 2851 }
3819 2852 }
3820 2853
3821 2854 // Global delegation for dynamically added links as fallback
3822 -// Use class selector for multi-instance support
3823 -$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
2855 +$(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
3824 2856 const $link = $(this);
3825 2857 const messageDiv = $link.closest('.bot-message, .agent-message');
3826 -
2858 +
3827 2859 // Only process bot/agent message links
3828 2860 if (messageDiv.length > 0) {
3829 2861 const originalHref = $link.attr('href');
3830 -
2862 +
3831 2863 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3832 2864 e.preventDefault();
3833 2865 e.stopPropagation();
3834 -
2866 +
3835 2867 // Mark as tracked
3836 2868 $link.attr('data-tracked', 'true');
3837 -
3838 - // Get bot ID from the chat box context
3839 - var botId = getBotIdFromElement(this);
3840 -
2869 +
3841 2870 // Get message context from the message div
3842 2871 const messageText = messageDiv.text().substring(0, 200);
3843 -
2872 +
3844 2873 $.ajax({
3845 2874 url: mxchatChat.ajax_url,
3846 2875 type: 'POST',
3847 2876 data: {
3848 2877 action: 'mxchat_track_url_click',
3849 - session_id: getChatSession(botId),
2878 + session_id: getChatSession(),
3850 2879 url: originalHref,
3851 2880 message_context: messageText,
3852 2881 nonce: mxchatChat.nonce
3853 2882 },
@@ -3858,58 +2887,50 @@
3858 2887 window.location.href = originalHref;
3859 2888 }
3860 2889 }
3861 2890 });
3862 -
2891 +
3863 2892 return false;
3864 2893 }
3865 2894 }
3866 2895 });
3867 2896
3868 - // ====================================
3869 - // MAIN INITIALIZATION
3870 - // ====================================
2897 +
2898 +
2899 +
2900 +// ====================================
2901 +// MAIN INITIALIZATION
2902 +// ====================================
3871 2903
3872 - // Initialize all chatbot instances on the page
3873 - initializeAllInstances();
2904 +if ($('#floating-chatbot').hasClass('hidden')) {
2905 + $('#floating-chatbot-button').removeClass('hidden');
2906 +}
2907 +// Initialize when document is ready
2908 +setFullHeight();
2909 +trackOriginatingPage();
3874 2910
3875 - // Legacy initialization for single bot compatibility
3876 - $('.floating-chatbot.hidden').each(function() {
3877 - var botId = getBotIdFromElement(this);
3878 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3879 - });
2911 +// Only load chat history if email collection is disabled
2912 +if (mxchatChat.email_collection_enabled !== 'on') {
2913 + loadChatHistory();
2914 +}
3880 2915
3881 - // Initialize when document is ready
3882 - setFullHeight();
2916 +initializeChatVisibility();
3883 2917
3884 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3885 - // until the user's first interaction via MxChatInstances.ensureSession()
2918 +// Make functions globally available for add-ons
2919 +window.hasQuickQuestions = hasQuickQuestions;
2920 +window.collapseQuickQuestions = collapseQuickQuestions;
2921 +window.appendMessage = appendMessage;
2922 +window.appendThinkingMessage = appendThinkingMessage;
2923 +window.scrollToBottom = scrollToBottom;
2924 +window.scrollElementToTop = scrollElementToTop;
2925 +window.replaceLastMessage = replaceLastMessage;
2926 +window.callMxChat = callMxChat;
2927 +window.callMxChatStream = callMxChatStream;
2928 +window.shouldUseStreaming = shouldUseStreaming;
2929 +window.getChatSession = getChatSession;
2930 +window.getPageContext = getPageContext;
2931 +window.updateStreamingMessage = updateStreamingMessage;
3886 2932
3887 - // Initialize chat visibility for all instances
3888 - $('.mxchat-chatbot-wrapper').each(function() {
3889 - var botId = $(this).data('bot-id') || 'default';
3890 - initializeChatVisibility(botId);
3891 - });
3892 -
3893 - // Make functions globally available for add-ons
3894 - window.hasQuickQuestions = hasQuickQuestions;
3895 - window.collapseQuickQuestions = collapseQuickQuestions;
3896 - window.appendMessage = appendMessage;
3897 - window.appendThinkingMessage = appendThinkingMessage;
3898 - window.scrollToBottom = scrollToBottom;
3899 - window.scrollElementToTop = scrollElementToTop;
3900 - window.replaceLastMessage = replaceLastMessage;
3901 - window.callMxChat = callMxChat;
3902 - window.callMxChatStream = callMxChatStream;
3903 - window.shouldUseStreaming = shouldUseStreaming;
3904 - window.getChatSession = getChatSession;
3905 - window.getPageContext = getPageContext;
3906 - window.updateStreamingMessage = updateStreamingMessage;
3907 - window.MxChatInstances = MxChatInstances;
3908 - window.getElement = getElement;
3909 - window.getElementDOM = getElementDOM;
3910 - window.getBotIdFromElement = getBotIdFromElement;
3911 -
3912 2933 }); // End of jQuery ready
3913 2934
3914 2935
3915 2936 // ====================================
@@ -3936,299 +2957,6 @@
3936 2957 }, 2000);
3937 2958 });
3938 2959 }
3939 2960 }
3940 -});
3941 -
3942 -// ============================================================================
3943 -// SATISFACTION RATING (v3.2.6)
3944 -// ============================================================================
3945 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
3946 -// inactivity following a bot reply. One prompt per session, deduped via
3947 -// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3948 -// is exactly false (default ON).
3949 -jQuery(function($) {
3950 - if (typeof mxchatChat === 'undefined') return;
3951 - if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
3952 -
3953 - // wp_localize_script stringifies ints, so accept both number and numeric string.
3954 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
3955 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
3956 - if (!isFinite(idleSeconds)) idleSeconds = 60;
3957 - if (idleSeconds < 5) idleSeconds = 5;
3958 - if (idleSeconds > 600) idleSeconds = 600;
3959 - var IDLE_MS = idleSeconds * 1000;
3960 - var MIN_BOT_REPLIES = 2;
3961 - var ratingState = {};
3962 -
3963 - function getState(botId) {
3964 - if (!ratingState[botId]) {
3965 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
3966 - }
3967 - return ratingState[botId];
3968 - }
3969 -
3970 - function getSessionId(botId) {
3971 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
3972 - return MxChatInstances.getChatSession(botId);
3973 - }
3974 - return null;
3975 - }
3976 -
3977 - function isAlreadyRated(sessionId) {
3978 - if (!sessionId) return false;
3979 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
3980 - }
3981 -
3982 - function markRated(sessionId) {
3983 - if (!sessionId) return;
3984 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
3985 - }
3986 -
3987 - function esc(s) {
3988 - return String(s == null ? '' : s)
3989 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3990 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
3991 - }
3992 -
3993 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
3994 - function ratingSkipInlineColors(botId) {
3995 - if (mxchatChat.skip_inline_colors) return true;
3996 - var botAssignments = mxchatChat.bot_theme_assignments || {};
3997 - return botAssignments.hasOwnProperty(botId);
3998 - }
3999 -
4000 - function botBubbleStyleAttr(botId) {
4001 - if (ratingSkipInlineColors(botId)) return '';
4002 - var bg = mxchatChat.bot_message_bg_color;
4003 - var fg = mxchatChat.bot_message_font_color;
4004 - if (!bg && !fg) return '';
4005 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4006 - }
4007 -
4008 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4009 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4010 - // and paints the filled "Send" pill so it fills with the bot font color and
4011 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4012 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4013 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4014 - // unreliably inside a descendant's `background`, so a bubble-level var would
4015 - // silently fall back to the literal (white-block bug all over again). Inline
4016 - // longhand always wins. Same transparent-guard as the menu so we never paint
4017 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4018 - function syncRatingBubbleColors(botId) {
4019 - var $chatBox = getChatBoxByBotId(botId);
4020 - if (!$chatBox || !$chatBox.length) return;
4021 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4022 - if (!bubbleEl) return;
4023 - var cs = window.getComputedStyle(bubbleEl);
4024 - var fg = cs.color;
4025 - var bg = cs.backgroundColor;
4026 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4027 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4028 - // Expose on the bubble too, for any inheriting styles / future use.
4029 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4030 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4031 - // Paint the Send pill directly — the part that actually fixes the bug.
4032 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4033 - if (submitEl) {
4034 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4035 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4036 - }
4037 - }
4038 -
4039 - function copy(key) {
4040 - var c = mxchatChat.satisfaction_rating_copy || {};
4041 - var d = {
4042 - question: 'Was this helpful?',
4043 - helpful: 'Helpful',
4044 - not_helpful: 'Not helpful',
4045 - dismiss: 'Dismiss',
4046 - thanks: 'Thanks! Anything we should improve? (optional)',
4047 - placeholder: 'Tell us what could be better…',
4048 - send: 'Send',
4049 - skip: 'Skip',
4050 - saved: 'Thanks for the feedback.'
4051 - };
4052 - return c[key] || d[key];
4053 - }
4054 -
4055 - function thumbUpSvg() {
4056 - return '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 0 1 6 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V2.75A.75.75 0 0 1 15 2a2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23h-.777Z"/><path d="M2.331 10.977a11.969 11.969 0 0 0-.831 4.398 12 12 0 0 0 .52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 0 1-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227Z"/></svg>';
4057 - }
4058 - function thumbDownSvg() {
4059 - return '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M15.73 5.25h1.035A7.465 7.465 0 0 1 18 9.375a7.465 7.465 0 0 1-1.235 4.125h-.148c-.806 0-1.534.446-2.031 1.08a9.04 9.04 0 0 1-2.861 2.4c-.723.384-1.35.956-1.653 1.715a4.498 4.498 0 0 0-.322 1.672V21a.75.75 0 0 1-.75.75 2.25 2.25 0 0 1-2.25-2.25c0-1.152.26-2.243.723-3.218.266-.558-.107-1.282-.725-1.282H3.622c-1.026 0-1.945-.694-2.054-1.715A12.137 12.137 0 0 1 1.5 12c0-2.848.992-5.464 2.649-7.521C4.537 3.997 5.136 3.75 5.754 3.75h4.541c.483 0 .964.078 1.423.23l3.114 1.04c.46.152.94.23 1.423.23Z"/><path d="M21.669 13.023c.536-1.362.831-2.845.831-4.398 0-1.22-.182-2.398-.52-3.507-.26-.85-1.084-1.368-1.973-1.368H19.1c-.445 0-.72.498-.523.898.591 1.2.924 2.55.924 3.977a8.958 8.958 0 0 1-1.302 4.666c-.245.403.028.959.5.959h1.053c.832 0 1.612-.453 1.918-1.227Z"/></svg>';
4060 - }
4061 -
4062 - function buildPromptHtml(botId) {
4063 - var styleAttr = botBubbleStyleAttr(botId);
4064 - return ''
4065 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4066 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4067 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4068 - + '<div class="mxchat-rating-actions">'
4069 - + '<span class="mxchat-rating-buttons">'
4070 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4071 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4072 - + '</span>'
4073 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4074 - + '</div>'
4075 - + '</div>'
4076 - + '</div>';
4077 - }
4078 -
4079 - function buildFeedbackHtml(botId, rating) {
4080 - var styleAttr = botBubbleStyleAttr(botId);
4081 - return ''
4082 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4083 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4084 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4085 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4086 - + '<div class="mxchat-rating-feedback-actions">'
4087 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4088 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4089 - + '</div>'
4090 - + '</div>'
4091 - + '</div>';
4092 - }
4093 -
4094 - function buildSavedHtml(botId) {
4095 - var styleAttr = botBubbleStyleAttr(botId);
4096 - return ''
4097 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4098 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4099 - + '</div>';
4100 - }
4101 -
4102 - function getChatBoxByBotId(botId) {
4103 - var $byId = $('#chat-box-' + botId);
4104 - if ($byId.length) return $byId.first();
4105 - return $('.chat-box').first();
4106 - }
4107 -
4108 - function scrollChatBoxToBottom($chatBox) {
4109 - if (!$chatBox || !$chatBox.length) return;
4110 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4111 - }
4112 -
4113 - function showPrompt(botId) {
4114 - var s = getState(botId);
4115 - if (s.promptShown || s.dismissed) return;
4116 - var sessionId = getSessionId(botId);
4117 - if (!sessionId) return;
4118 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4119 - var $chatBox = getChatBoxByBotId(botId);
4120 - if (!$chatBox.length) return;
4121 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4122 - $chatBox.append(buildPromptHtml(botId));
4123 - syncRatingBubbleColors(botId);
4124 - s.promptShown = true;
4125 - scrollChatBoxToBottom($chatBox);
4126 - }
4127 -
4128 - function submitRating(botId, rating, feedback) {
4129 - var sessionId = getSessionId(botId);
4130 - if (!sessionId) return;
4131 - $.post(mxchatChat.ajax_url, {
4132 - action: 'mxchat_save_rating',
4133 - session_id: sessionId,
4134 - bot_id: botId,
4135 - rating: rating,
4136 - feedback: feedback || ''
4137 - });
4138 - markRated(sessionId);
4139 - }
4140 -
4141 - function onBotReply(botId) {
4142 - var s = getState(botId);
4143 - s.botReplies += 1;
4144 - if (s.promptShown || s.dismissed) return;
4145 - var sessionId = getSessionId(botId);
4146 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4147 - if (s.botReplies < MIN_BOT_REPLIES) return;
4148 - if (s.idleTimer) clearTimeout(s.idleTimer);
4149 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4150 - }
4151 -
4152 - function onUserMessage(botId) {
4153 - var s = getState(botId);
4154 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4155 - }
4156 -
4157 - function botIdFromChatBox(el) {
4158 - var id = el && el.id ? el.id : '';
4159 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4160 - }
4161 -
4162 - function setupObserver(chatBox) {
4163 - var botId = botIdFromChatBox(chatBox);
4164 - try {
4165 - var observer = new MutationObserver(function(mutations) {
4166 - mutations.forEach(function(m) {
4167 - for (var i = 0; i < m.addedNodes.length; i++) {
4168 - var node = m.addedNodes[i];
4169 - if (!node || node.nodeType !== 1) continue;
4170 - var $n = $(node);
4171 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4172 - if ($n.hasClass('bot-message')) onBotReply(botId); // count at insert time — streaming providers append with .temporary-message first, then remove later (childList observer can't see attr changes)
4173 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4174 - }
4175 - });
4176 - });
4177 - observer.observe(chatBox, { childList: true });
4178 - } catch (e) { /* noop */ }
4179 - }
4180 -
4181 - $('.chat-box').each(function() { setupObserver(this); });
4182 -
4183 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4184 - e.preventDefault();
4185 - var $btn = $(this);
4186 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4187 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4188 - var botId = $prompt.data('bot-id') || 'default';
4189 - var rating = parseInt($btn.attr('data-rating'), 10);
4190 - if (rating !== 1 && rating !== -1) return;
4191 - submitRating(botId, rating, '');
4192 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4193 - syncRatingBubbleColors(botId);
4194 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4195 - });
4196 -
4197 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4198 - e.preventDefault();
4199 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4200 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4201 - var botId = $prompt.data('bot-id') || 'default';
4202 - var s = getState(botId);
4203 - s.dismissed = true;
4204 - markRated(getSessionId(botId));
4205 - ($wrap.length ? $wrap : $prompt).remove();
4206 - });
4207 -
4208 - function closeFeedback($fb) {
4209 - var botId = $fb.data('bot-id') || 'default';
4210 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4211 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4212 - syncRatingBubbleColors(botId);
4213 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4214 - }
4215 -
4216 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4217 - e.preventDefault();
4218 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4219 - });
4220 -
4221 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4222 - e.preventDefault();
4223 - var $fb = $(this).closest('.mxchat-rating-feedback');
4224 - var botId = $fb.data('bot-id') || 'default';
4225 - var rating = parseInt($fb.attr('data-rating'), 10);
4226 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4227 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4228 - if (text !== '') {
4229 - submitRating(botId, rating, text);
4230 - }
4231 - closeFeedback($fb);
4232 - });
4233 2961 });
4234 2962