PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.2
MxChat – AI Chatbot & Content Generation for WordPress v2.6.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +1220 -2710 3.2.132.6.2 View file →
@@ -1,383 +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 -
108 - // Dynamic-settings refresh (plan-32db95).
109 - //
110 - // Every widget setting ships inline in cached page HTML, so behind a
111 - // full-page cache the site owner can't purge (host cache, CDN, the
112 - // browser itself) a toggled setting looks broken until the cache turns
113 - // over. Same distrust-cached-HTML reasoning as the per-request nonce:
114 - // on the FIRST widget open per page load we ask the nonce endpoint for
115 - // the current behavior-gate settings (?with_settings=1), merge them over
116 - // mxchatChat, and rebuild the header menu. Colors are NOT refreshed —
117 - // they're server-inline-styled, so a runtime swap would visibly flash.
118 - // On any failure we keep the inline values silently (nonce-fallback
119 - // posture). At most one request per page load, only if a widget opens.
120 - var dynamicSettingsState = 'idle'; // 'idle' | 'pending' | 'done'
121 -
122 - function mxchatRefreshDynamicSettings() {
123 - if (dynamicSettingsState !== 'idle') return;
124 - if (typeof mxchatChat === 'undefined') return;
125 - dynamicSettingsState = 'pending';
126 -
127 - var applied = function (data) {
128 - dynamicSettingsState = 'done';
129 - if (!data) return; // endpoint unavailable — inline values stand.
130 - if (data.nonce) {
131 - // Seed the nonce cache too: saves the first send's REST
132 - // round-trip and keeps us under the endpoint's rate limit.
133 - cachedFreshNonce = data.nonce;
134 - cachedFreshNonceFetchedAt = Date.now();
135 - mxchatChat.nonce = data.nonce;
136 - }
137 - if (data.settings && typeof data.settings === 'object') {
138 - $.extend(mxchatChat, data.settings);
139 - mxchatRebuildHeaderMenus();
140 - }
141 - };
142 -
143 - fetch(getRestNonceUrl() + '?with_settings=1', {
144 - credentials: 'same-origin',
145 - headers: { 'Accept': 'application/json' }
146 - }).then(function (resp) {
147 - if (!resp.ok) throw new Error('settings refresh failed: ' + resp.status);
148 - return resp.json();
149 - }).then(applied).catch(function () {
150 - // Fallback: legacy admin-ajax refresh path, same as withFreshNonce.
151 - if (mxchatChat.ajax_url) {
152 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce', with_settings: 1 })
153 - .done(function (res) {
154 - applied(res && res.success && res.data ? res.data : null);
155 - })
156 - .fail(function () { applied(null); });
157 - } else {
158 - applied(null);
159 - }
160 - });
161 - }
162 -
2 +
163 3 // ====================================
164 - // MULTI-INSTANCE MANAGEMENT SYSTEM
165 - // ====================================
166 -
167 - // Instance registry - tracks all chatbot instances on the page
168 - const MxChatInstances = {
169 - instances: {},
170 -
171 - // Initialize an instance for a bot
172 - init: function(botId) {
173 - if (!this.instances[botId]) {
174 - // When persistence is OFF, track when this session started
175 - // so the AI only sees messages from this page load
176 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
177 -
178 - this.instances[botId] = {
179 - botId: botId,
180 - sessionId: null,
181 - lastSeenMessageId: '',
182 - notificationCheckInterval: null,
183 - pollingInterval: null,
184 - processedMessageIds: new Set(),
185 - activePdfFile: null,
186 - activeWordFile: null,
187 - chatHistoryLoaded: false,
188 - isStreaming: false,
189 - // Fresh context timestamp - only used when persistence is OFF
190 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
191 - };
192 - }
193 - return this.instances[botId];
194 - },
195 -
196 - // Get instance by botId
197 - get: function(botId) {
198 - return this.instances[botId] || this.init(botId);
199 - },
200 -
201 - // Get all active bot IDs
202 - getAllBotIds: function() {
203 - return Object.keys(this.instances);
204 - },
205 -
206 - // Session management per bot
207 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
208 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
209 - getChatSession: function(botId) {
210 - var cookieName = 'mxchat_session_id_' + botId;
211 - var storageKey = 'mxchat_session_id_' + botId;
212 - var sessionId = getCookie(cookieName);
213 -
214 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
215 - if (!sessionId) {
216 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
217 - }
218 -
219 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
220 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
221 - // storage). Without this, ensureSession() can generate and store an ID that
222 - // getChatSession() then can't read back, causing null session_ids on send.
223 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
224 - sessionId = this.instances[botId].sessionId;
225 - }
226 -
227 - // Guard against stored sentinel values that indicate earlier broken writes.
228 - if (sessionId === 'null' || sessionId === 'undefined') {
229 - sessionId = null;
230 - }
231 -
232 - // Re-sync cookie from localStorage if cookie was lost
233 - if (sessionId && !getCookie(cookieName)) {
234 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
235 - }
236 -
237 - return sessionId || null;
238 - },
239 -
240 - // Lazy session initializer — called on first user interaction
241 - ensureSession: function(botId) {
242 - botId = botId || 'default';
243 - var instance = this.instances[botId] || this.init(botId);
244 -
245 - if (instance.sessionId) {
246 - return instance.sessionId;
247 - }
248 -
249 - // Check for existing session from cookie or localStorage
250 - var existingSession = this.getChatSession(botId);
251 -
252 - if (existingSession) {
253 - instance.sessionId = existingSession;
254 - } else {
255 - // Brand new session
256 - var newId = generateSessionId();
257 - this.setChatSession(botId, newId);
258 - instance.sessionId = newId;
259 - }
260 -
261 - // Now that we have a session, do the deferred work
262 - refreshNonceIfNeeded();
263 - trackOriginatingPage();
264 -
265 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
266 - // so we do NOT call it here to avoid a race condition.
267 -
268 - return instance.sessionId;
269 - },
270 -
271 - setChatSession: function(botId, sessionId) {
272 - var cookieName = 'mxchat_session_id_' + botId;
273 - var storageKey = 'mxchat_session_id_' + botId;
274 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
275 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
276 - if (this.instances[botId]) {
277 - this.instances[botId].sessionId = sessionId;
278 - }
279 - },
280 -
281 - resetChatSession: function(botId) {
282 - // Clear old session from localStorage before setting new one
283 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
284 - var newSessionId = generateSessionId();
285 - this.setChatSession(botId, newSessionId);
286 - var $chatBox = getElement(botId, 'chat-box');
287 - if ($chatBox.length) {
288 - $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
289 - }
290 - if (this.instances[botId]) {
291 - this.instances[botId].chatHistoryLoaded = false;
292 - this.instances[botId].processedMessageIds = new Set();
293 - }
294 - },
295 -
296 - // Silent reset — new session ID without clearing the chat UI
297 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
298 - silentResetSession: function(botId) {
299 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
300 - var newSessionId = generateSessionId();
301 - this.setChatSession(botId, newSessionId);
302 - if (this.instances[botId]) {
303 - this.instances[botId].sessionId = newSessionId;
304 - }
305 - return newSessionId;
306 - }
307 - };
308 -
309 - // ====================================
310 - // ELEMENT SELECTOR HELPERS
311 - // ====================================
312 -
313 - // Check if a specific bot has an AI theme assigned (skip inline colors)
314 - function shouldSkipInlineColors(botId) {
315 - // If global AI theme is active, skip inline colors for all bots
316 - if (mxchatChat.skip_inline_colors) {
317 - return true;
318 - }
319 - // Check if this specific bot has a theme assignment
320 - var botAssignments = mxchatChat.bot_theme_assignments || {};
321 - return botAssignments.hasOwnProperty(botId);
322 - }
323 -
324 - // Get element by ID with bot suffix - returns jQuery object
325 - function getElement(botId, elementName) {
326 - return $('#' + elementName + '-' + botId);
327 - }
328 -
329 - // Get element by ID with bot suffix - returns DOM element
330 - function getElementDOM(botId, elementName) {
331 - return document.getElementById(elementName + '-' + botId);
332 - }
333 -
334 - // Get bot ID from any element within a chatbot instance
335 - function getBotIdFromElement(element) {
336 - var $wrapper = $(element).closest('.mxchat-chatbot-wrapper');
337 - if ($wrapper.length) {
338 - return $wrapper.data('bot-id') || 'default';
339 - }
340 - // Fallback: try to find from floating container
341 - var $floating = $(element).closest('.floating-chatbot');
342 - if ($floating.length) {
343 - var id = $floating.attr('id') || '';
344 - var match = id.match(/floating-chatbot-(.+)/);
345 - if (match) return match[1];
346 - }
347 - // Fallback: the pre-chat teaser bubble (#pre-chat-message-{bot_id}) is a SIBLING
348 - // outside .mxchat-chatbot-wrapper / .floating-chatbot, so its children — e.g. the
349 - // .close-pre-chat-message button, which carries only a class and no id — miss both
350 - // branches above. Walk to the nearest ancestor whose id is pre-chat-message-{bot_id}
351 - // and read the suffix. (closest() includes the element itself, so a click directly on
352 - // #pre-chat-message-{bot_id} resolves here too.)
353 - var $preChat = $(element).closest('[id^="pre-chat-message-"]');
354 - if ($preChat.length) {
355 - var preId = $preChat.attr('id') || '';
356 - var preMatch = preId.match(/^pre-chat-message-(.+)$/);
357 - if (preMatch) return preMatch[1];
358 - }
359 - // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
360 - var elementId = $(element).attr('id') || '';
361 - if (elementId) {
362 - // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
363 - var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+)$/);
364 - if (idMatch) return idMatch[1];
365 - }
366 - return 'default';
367 - }
368 -
369 - // Get wrapper element for a bot
370 - function getWrapper(botId) {
371 - return getElement(botId, 'mxchat-chatbot-wrapper');
372 - }
373 -
374 - // ====================================
375 4 // GLOBAL VARIABLES & CONFIGURATION
376 5 // ====================================
377 6 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
378 -
379 - // Initialize color settings (these are global as they come from PHP)
7 +
8 + // Initialize color settings
380 9 var userMessageBgColor = mxchatChat.user_message_bg_color;
381 10 var userMessageFontColor = mxchatChat.user_message_font_color;
382 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
383 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
@@ -382,64 +11,64 @@
382 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
383 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
384 13 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
385 14 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
386 -
15 +
387 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;
388 26
389 27 // ====================================
390 - // SESSION MANAGEMENT (Legacy compatibility)
28 + // SESSION MANAGEMENT
391 29 // ====================================
392 -
30 +
31 + function getChatSession() {
32 + var sessionId = getCookie('mxchat_session_id');
33 + //console.log("Session ID retrieved from cookie: ", sessionId);
34 +
35 + if (!sessionId) {
36 + sessionId = generateSessionId();
37 + //console.log("Generated new session ID: ", sessionId);
38 + setChatSession(sessionId);
39 + }
40 +
41 + //console.log("Final session ID: ", sessionId);
42 + return sessionId;
43 + }
44 +
45 + function setChatSession(sessionId) {
46 + // Set the cookie with a 24-hour expiration (86400 seconds)
47 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
48 + }
49 +
393 50 function getCookie(name) {
394 51 let value = "; " + document.cookie;
395 52 let parts = value.split("; " + name + "=");
396 53 if (parts.length == 2) return parts.pop().split(";").shift();
397 54 }
398 -
55 +
399 56 function generateSessionId() {
400 57 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
401 58 }
402 59
403 - // Legacy function - now delegates to instance manager
404 - function getChatSession(botId) {
405 - botId = botId || 'default';
406 - return MxChatInstances.getChatSession(botId);
60 + function resetChatSession() {
61 + // Generate a new session ID
62 + var newSessionId = generateSessionId();
63 + // Update the cookie with the new session
64 + setChatSession(newSessionId);
65 + // Clear the chat history display
66 + $('.mxchat-messages').empty();
67 + // Reset the chat history loaded flag
68 + chatHistoryLoaded = false;
407 69 }
408 70
409 - function setChatSession(sessionId, botId) {
410 - botId = botId || 'default';
411 - MxChatInstances.setChatSession(botId, sessionId);
412 - }
413 -
414 - function resetChatSession(botId) {
415 - botId = botId || 'default';
416 - MxChatInstances.resetChatSession(botId);
417 - }
418 -
419 - // ====================================
420 - // INITIALIZE ALL CHATBOT INSTANCES
421 - // ====================================
422 -
423 - function initializeAllInstances() {
424 - // Find all chatbot wrappers on the page
425 - $('.mxchat-chatbot-wrapper').each(function() {
426 - var botId = $(this).data('bot-id') || 'default';
427 - MxChatInstances.init(botId);
428 - initializeBotInstance(botId);
429 - });
430 - }
431 -
432 - function initializeBotInstance(botId) {
433 - var instance = MxChatInstances.get(botId);
434 -
435 - // Initialize quick questions state for this bot
436 - checkQuickQuestionsState(botId);
437 -
438 - // Note: Event handlers use event delegation with class selectors,
439 - // so they work automatically for all instances without per-bot setup
440 - }
441 -
442 71 // ====================================
443 72 // CONTEXTUAL AWARENESS FUNCTIONALITY
444 73 // ====================================
445 74
@@ -584,216 +213,70 @@
584 213
585 214 // ====================================
586 215 // CORE CHAT FUNCTIONALITY
587 216 // ====================================
588 -
589 -// Helper functions to disable/enable chat input while waiting for response
590 -function disableChatInput(botId) {
591 - botId = botId || 'default';
592 - var chatInput = getElementDOM(botId, 'chat-input');
593 - var sendButton = getElementDOM(botId, 'send-button');
594 - if (chatInput) {
595 - chatInput.disabled = true;
596 - chatInput.style.opacity = '0.6';
597 - }
598 - if (sendButton) {
599 - sendButton.disabled = true;
600 - sendButton.style.opacity = '0.5';
601 - sendButton.style.pointerEvents = 'none';
602 - }
603 -}
604 -
605 -function enableChatInput(botId) {
606 - botId = botId || 'default';
607 - var chatInput = getElementDOM(botId, 'chat-input');
608 - var sendButton = getElementDOM(botId, 'send-button');
609 - if (chatInput) {
610 - chatInput.disabled = false;
611 - chatInput.style.opacity = '1';
612 - chatInput.focus();
613 - }
614 - if (sendButton) {
615 - sendButton.disabled = false;
616 - sendButton.style.opacity = '1';
617 - sendButton.style.pointerEvents = 'auto';
618 - }
619 - // Every completion path re-enables input, so this is the single restore
620 - // point for the streaming Stop affordance (no-op when not in stop mode).
621 - mxchatRestoreSendButton(botId);
622 -}
623 -
624 -// --- Streaming Stop control -------------------------------------------------
625 -// One live stream handle per bot instance, so Stop on one widget never aborts
626 -// another bot on the same page.
627 -var mxchatActiveStreams = {};
628 -// Original send-button markup, captured once per bot the first time the Stop
629 -// state is shown (never captured while already in stop mode, so a rapid
630 -// stop-then-resend can't save the stop glyph as the "original").
631 -var mxchatSendMarkup = {};
632 -
633 -function mxchatShowStopButton(botId) {
634 - var btn = getElementDOM(botId, 'send-button');
635 - if (!btn) return;
636 - if (!btn.classList.contains('mxchat-stop-mode')) {
637 - mxchatSendMarkup[botId] = {
638 - html: btn.innerHTML,
639 - label: btn.getAttribute('aria-label')
640 - };
641 - }
642 -
643 - // Mirror the send icon's rendered size + color so the stop glyph looks
644 - // native, including custom send images/colors and theme overrides.
645 - var child = btn.querySelector('svg, img');
646 - var size = 25;
647 - var color = '';
648 - if (child) {
649 - var rect = child.getBoundingClientRect();
650 - if (rect.width) {
651 - size = Math.round(Math.min(rect.width, rect.height));
652 - }
653 - var cs = window.getComputedStyle(child);
654 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
655 - }
656 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
657 - btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" style="width:' + size + 'px;height:' + size + 'px;' + (color ? 'fill:' + color + ';' : '') + '"><rect x="5" y="5" width="14" height="14" rx="3"></rect></svg>';
658 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
659 - // start a stream synchronously while the originating click is still
660 - // bubbling up to our delegated handler. Without this guard, that handler
661 - // reads the just-added stop-mode class as a user Stop press and aborts the
662 - // brand-new stream — the user's message renders but no reply ever fires
663 - // (plan-4bba64 silent message loss). The flag only spans the current event
664 - // dispatch: cleared on the next macrotask, long before a real Stop click.
665 - btn.__mxchatStopJustShown = true;
666 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
667 - btn.classList.add('mxchat-stop-mode');
668 - btn.setAttribute('aria-label', stopLabel);
669 - btn.setAttribute('title', stopLabel);
670 - // disableChatInput() ran when the turn was sent; the Stop control itself
671 - // must stay clickable while the textarea remains disabled.
672 - btn.disabled = false;
673 - btn.style.opacity = '1';
674 - btn.style.pointerEvents = 'auto';
675 -}
676 -
677 -function mxchatRestoreSendButton(botId) {
678 - var btn = getElementDOM(botId, 'send-button');
679 - var saved = mxchatSendMarkup[botId];
680 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
681 - btn.innerHTML = saved.html;
682 - btn.classList.remove('mxchat-stop-mode');
683 - btn.removeAttribute('title');
684 - if (saved.label) {
685 - btn.setAttribute('aria-label', saved.label);
686 - }
687 -}
688 -
689 -function mxchatStopStreaming(botId) {
690 - var entry = mxchatActiveStreams[botId];
691 - if (!entry || !entry.controller) return;
692 - entry.aborted = true;
693 - try { entry.controller.abort(); } catch (e) {}
694 -}
695 -
696 -// Returns true when a stream rejection came from an intentional Stop click:
697 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
698 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
699 - var entry = mxchatActiveStreams[botId];
700 - if (!entry || !entry.aborted) return false;
701 - delete mxchatActiveStreams[botId];
702 - if (!accumulatedContent) {
703 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
704 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
705 - }
706 - enableChatInput(botId); // also restores the send icon
707 - if (callback) {
708 - callback(accumulatedContent || '');
709 - }
710 - return true;
711 -}
712 -
713 217 // Update your existing sendMessage function
714 -function sendMessage(botId) {
715 - botId = botId || 'default';
716 - MxChatInstances.ensureSession(botId);
717 - var $chatInput = getElement(botId, 'chat-input');
718 - var message = $chatInput.val();
719 -
218 +function sendMessage() {
219 + var message = $('#chat-input').val();
220 +
720 221 // ADD PROMPT HOOK HERE
721 - if (typeof customMxChatFilter === 'function') {
722 - message = customMxChatFilter(message, "prompt");
222 + if (typeof customMxChatFilter === 'function') {
223 + message = customMxChatFilter(message, "prompt");
723 224 }
724 -
225 +
725 226 if (message) {
726 - // Don't disable input in live agent mode - let users chat freely
727 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
728 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
729 - if (!isAgentMode) {
730 - disableChatInput(botId);
731 - }
227 + appendMessage("user", message);
228 + $('#chat-input').val('');
229 + $('#chat-input').css('height', 'auto');
732 230
733 - appendMessage("user", message, '', [], false, botId);
734 - $chatInput.val('');
735 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
736 - $chatInput.css('height', 'auto');
737 -
738 - if (hasQuickQuestions(botId)) {
739 - collapseQuickQuestions(botId);
231 + if (hasQuickQuestions()) {
232 + collapseQuickQuestions();
740 233 }
741 - appendThinkingMessage(botId);
742 - scrollToBottom(botId);
234 + appendThinkingMessage();
235 + scrollToBottom();
743 236
744 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
237 + const currentModel = mxchatChat.model || 'gpt-4o';
745 238
746 239 // Check if streaming is enabled AND supported for this model
747 240 if (shouldUseStreaming(currentModel)) {
748 241 callMxChatStream(message, function(response) {
749 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
750 - }, botId);
242 + $('.bot-message.temporary-message').removeClass('temporary-message');
243 + });
751 244 } else {
752 245 callMxChat(message, function(response) {
753 - replaceLastMessage("bot", response, '', [], botId);
754 - }, botId);
246 + replaceLastMessage("bot", response);
247 + });
755 248 }
756 249 }
757 250 }
758 251
759 252 // Update your existing sendMessageToChatbot function
760 -function sendMessageToChatbot(message, botId) {
761 - botId = botId || 'default';
762 - MxChatInstances.ensureSession(botId);
763 -
253 +function sendMessageToChatbot(message) {
764 254 // ADD PROMPT HOOK HERE
765 255 if (typeof customMxChatFilter === 'function') {
766 256 message = customMxChatFilter(message, "prompt");
767 257 }
258 +
259 + var sessionId = getChatSession();
768 260
769 - // Don't disable input in live agent mode - let users chat freely
770 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
771 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
772 - if (!isAgentMode) {
773 - disableChatInput(botId);
261 + if (hasQuickQuestions()) {
262 + collapseQuickQuestions();
774 263 }
264 + appendThinkingMessage();
265 + scrollToBottom();
775 266
776 - var sessionId = getChatSession(botId);
267 + const currentModel = mxchatChat.model || 'gpt-4o';
777 268
778 - if (hasQuickQuestions(botId)) {
779 - collapseQuickQuestions(botId);
780 - }
781 - appendThinkingMessage(botId);
782 - scrollToBottom(botId);
783 -
784 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
785 -
786 269 // Check if streaming is enabled AND supported for this model
787 270 if (shouldUseStreaming(currentModel)) {
788 271 callMxChatStream(message, function(response) {
789 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
790 - }, botId);
272 + $('.bot-message.temporary-message').removeClass('temporary-message');
273 + });
791 274 } else {
792 275 callMxChat(message, function(response) {
793 - getElement(botId, 'chat-box').find('.temporary-message').remove();
794 - replaceLastMessage("bot", response, '', [], botId);
795 - }, botId);
276 + $('.temporary-message').remove();
277 + replaceLastMessage("bot", response);
278 + });
796 279 }
797 280 }
798 281
799 282 // Updated shouldUseStreaming function with debugging
@@ -836,72 +319,40 @@
836 319 updateChatModeIndicator('agent');
837 320 }
838 321 }
839 322
840 -// Function to get bot ID from any element or wrapper
841 -// If element is provided, finds the bot ID from its wrapper
842 -// If no element, returns 'default' (for backward compatibility)
843 -function getMxChatBotId(element) {
844 - if (element) {
845 - return getBotIdFromElement(element);
846 - }
847 - // Fallback: find first chatbot wrapper on page
848 - const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper');
323 +//Function to get bot ID from the chatbot wrapper
324 +function getMxChatBotId() {
325 + const chatbotWrapper = document.getElementById('mxchat-chatbot-wrapper');
849 326 return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
850 327 }
851 328
852 -function callMxChat(message, callback, botId) {
853 - botId = botId || getMxChatBotId();
854 -
855 - // Streaming fallbacks land here: drop any leftover stream handle and
856 - // return the button to its send state (no-op for plain non-stream turns).
857 - if (mxchatActiveStreams[botId]) {
858 - delete mxchatActiveStreams[botId];
859 - }
860 - mxchatRestoreSendButton(botId);
861 -
329 +function callMxChat(message, callback) {
862 330 // Store the message in case we need to retry after session reset
863 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
331 + $('.mxchat-input-holder textarea').data('pending-message', message);
864 332
865 333 // Get page context if contextual awareness is enabled
866 334 const pageContext = getPageContext();
867 335
868 - // Get instance for session start timestamp (used when persistence is OFF)
869 - var instance = MxChatInstances.get(botId);
336 + // Get bot ID
337 + const botId = getMxChatBotId();
870 338
871 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
872 - // and returns the guaranteed-present session id from the in-memory instance even when
873 - // cookie/localStorage writes are silently blocked by the browser.
874 - var sessionId = MxChatInstances.ensureSession(botId);
875 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
876 - // Last-resort generation to ensure we never POST a null marker.
877 - sessionId = generateSessionId();
878 - MxChatInstances.setChatSession(botId, sessionId);
879 - }
880 -
881 - // Wait for the page-cache nonce refresh to complete before firing the
882 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
883 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
884 - // callback guarantees we read the fresh value. See plan-c5457f.
885 - refreshNonceIfNeeded(function() {
886 339 // Prepare AJAX data
887 340 const ajaxData = {
888 341 action: 'mxchat_handle_chat_request',
889 342 message: message,
890 - session_id: sessionId,
343 + session_id: getChatSession(),
891 344 nonce: mxchatChat.nonce,
892 345 current_page_url: window.location.href,
893 346 current_page_title: document.title,
894 - bot_id: botId,
895 - // Pass session start timestamp so AI context matches what user sees
896 - session_start_timestamp: instance.sessionStartTimestamp || 0
347 + bot_id: botId // Include bot ID
897 348 };
898 -
349 +
899 350 // Add page context if available
900 351 if (pageContext) {
901 352 ajaxData.page_context = JSON.stringify(pageContext);
902 353 }
903 -
354 +
904 355 // CHECK FOR VISION FLAGS AND ADD THEM
905 356 if (window.mxchatVisionProcessed) {
906 357 ajaxData.vision_processed = true;
907 358 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -910,9 +361,9 @@
910 361 window.mxchatVisionProcessed = false;
911 362 window.mxchatOriginalMessage = null;
912 363 window.mxchatVisionImagesCount = 0;
913 364 }
914 -
365 +
915 366 $.ajax({
916 367 url: mxchatChat.ajax_url,
917 368 type: 'POST',
918 369 dataType: 'json',
@@ -917,18 +368,29 @@
917 368 type: 'POST',
918 369 dataType: 'json',
919 370 data: ajaxData,
920 371 success: function(response) {
921 - // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
372 +
373 + // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
374 + if (response.chat_mode) {
375 + //console.log("Updating chat mode to:", response.chat_mode);
376 + updateChatModeIndicator(response.chat_mode);
377 + }
378 +
379 + // Also check in data property if response is wrapped
380 + if (response.data && response.data.chat_mode) {
381 + //console.log("Updating chat mode from data to:", response.data.chat_mode);
382 + updateChatModeIndicator(response.data.chat_mode);
383 + }
384 + // Log the full response for debugging
385 + //console.log("API Response:", response);
386 +
387 + // IMMEDIATE CHAT MODE UPDATE - Handle this first, before any other processing
922 388 if (response.chat_mode) {
923 - updateChatModeIndicator(response.chat_mode, botId);
389 + //console.log("Updating chat mode immediately to:", response.chat_mode);
390 + updateChatModeIndicator(response.chat_mode);
924 391 }
925 392
926 - // Also check in data property if response is wrapped
927 - if (response.data && response.data.chat_mode) {
928 - updateChatModeIndicator(response.data.chat_mode, botId);
929 - }
930 -
931 393 // SECURITY FIX: Check for errors FIRST before checking for success
932 394 // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
933 395 if (response.success === false || (response.data && response.data.error_message)) {
934 396 let errorMessage = "";
@@ -950,29 +412,40 @@
950 412 errorMessage = "An error occurred. Please try again or contact support.";
951 413 }
952 414
953 415 // Handle session reset action (IP changed, session expired, etc.)
954 - // Silent reset — keep chat UI intact, just get a new session and retry
955 416 if (response.data && response.data.action === 'reset_session') {
956 - MxChatInstances.silentResetSession(botId);
957 - // Re-send the original message with the new session (user message is already displayed)
958 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
417 + // Clear the old session and generate a new one
418 + resetChatSession();
419 + // Remove the temporary loading message
420 + $('.bot-message.temporary-message').remove();
421 + // Re-send the original message with the new session
422 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
959 423 if (originalMessage) {
960 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
961 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
424 + $('.mxchat-input-holder textarea').data('pending-message', null);
425 + // Re-add the user message and thinking indicator
426 + appendMessage("user", originalMessage);
427 + appendThinkingMessage();
428 + scrollToBottom();
429 + // Determine whether to use streaming
430 + const currentModel = mxchatChat.model || 'gpt-4o';
962 431 if (shouldUseStreaming(currentModel)) {
963 432 callMxChatStream(originalMessage, function(response) {
964 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
965 - }, botId);
433 + $('.bot-message.temporary-message').removeClass('temporary-message');
434 + });
966 435 } else {
967 436 callMxChat(originalMessage, function(response) {
968 - replaceLastMessage("bot", response, '', [], botId);
969 - }, botId);
437 + replaceLastMessage("bot", response);
438 + });
970 439 }
971 440 }
972 441 return;
973 442 }
974 443
444 + // Log the error with code for debugging
445 + //console.log("Response data:", response.data);
446 + //console.error("API Error:", errorMessage, "Code:", errorCode);
447 +
975 448 // Format user-friendly error message
976 449 let displayMessage = errorMessage;
977 450
978 451 // Customize message for admin users
@@ -980,9 +453,9 @@
980 453 // For admin users, show more technical details including error code
981 454 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
982 455 }
983 456
984 - replaceLastMessage("bot", displayMessage, '', [], botId);
457 + replaceLastMessage("bot", displayMessage);
985 458 return; // Exit early for errors
986 459 }
987 460
988 461 // NOW check if this is a successful response by looking for text, html, or message fields
@@ -998,17 +471,16 @@
998 471 let responseMessage = response.message || '';
999 472
1000 473 // Add PDF filename handling
1001 474 if (response.data && response.data.filename) {
1002 - showActivePdf(response.data.filename, botId);
1003 - var instance = MxChatInstances.get(botId);
1004 - instance.activePdfFile = response.data.filename;
475 + showActivePdf(response.data.filename);
476 + activePdfFile = response.data.filename;
1005 477 }
1006 478
1007 479 // Add redirect check here
1008 480 if (response.redirect_url) {
1009 481 if (responseText) {
1010 - replaceLastMessage("bot", responseText, '', [], botId);
482 + replaceLastMessage("bot", responseText);
1011 483 }
1012 484 setTimeout(() => {
1013 485 window.location.href = response.redirect_url;
1014 486 }, 1500);
@@ -1016,11 +488,9 @@
1016 488 }
1017 489
1018 490 // Check for live agent response
1019 491 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1020 - removeThinkingDots(botId);
1021 - updateChatModeIndicator('agent', botId);
1022 - enableChatInput(botId);
492 + updateChatModeIndicator('agent');
1023 493 return;
1024 494 }
1025 495
1026 496 // Handle the message and show notification if chat is hidden
@@ -1035,36 +505,32 @@
1035 505 }
1036 506
1037 507 // Update the messages as before
1038 508 if (responseText && responseHtml) {
1039 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
509 + replaceLastMessage("bot", responseText, responseHtml);
1040 510 } else if (responseText) {
1041 - replaceLastMessage("bot", responseText, '', [], botId);
511 + replaceLastMessage("bot", responseText);
1042 512 } else if (responseHtml) {
1043 - replaceLastMessage("bot", "", responseHtml, [], botId);
513 + replaceLastMessage("bot", "", responseHtml);
1044 514 } else if (responseMessage) {
1045 - replaceLastMessage("bot", responseMessage, '', [], botId);
515 + replaceLastMessage("bot", responseMessage);
1046 516 }
1047 517
1048 518 // Check if chat is hidden and show notification
1049 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1050 - if ($floatingChatbot.hasClass('hidden')) {
1051 - var $badge = getElement(botId, 'chat-notification-badge');
1052 - if ($badge.length) {
1053 - $badge.show();
519 + if ($('#floating-chatbot').hasClass('hidden')) {
520 + const badge = $('#chat-notification-badge');
521 + if (badge.length) {
522 + badge.show();
1054 523 }
1055 524 }
1056 525 } else {
1057 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1058 - if (response.vectorstore_error) {
1059 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1060 - }
1061 - replaceLastMessage("bot", emptyMsg, '', [], botId);
526 + // This should rarely happen now that errors are handled above
527 + ////console.error("Unexpected response format:", response);
528 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
1062 529 }
1063 530
1064 531 if (response.message_id) {
1065 - var instance = MxChatInstances.get(botId);
1066 - instance.lastSeenMessageId = response.message_id;
532 + lastSeenMessageId = response.message_id;
1067 533 }
1068 534
1069 535 return;
1070 536 }
@@ -1069,16 +535,20 @@
1069 535 return;
1070 536 }
1071 537
1072 538 // Fallback for truly unexpected response formats
1073 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
539 + replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.");
1074 540 },
1075 541 error: function(xhr, status, error) {
542 + //console.error("AJAX Error:", status, error);
543 + //console.log("Response Text:", xhr.responseText);
544 +
1076 545 let errorMessage = "An unexpected error occurred.";
1077 546
1078 547 // Try to parse the response if it's JSON
1079 548 try {
1080 549 const responseJson = JSON.parse(xhr.responseText);
550 + //console.log("Parsed error response:", responseJson);
1081 551
1082 552 if (responseJson.data && responseJson.data.error_message) {
1083 553 errorMessage = responseJson.data.error_message;
1084 554 } else if (responseJson.message) {
@@ -1098,56 +568,41 @@
1098 568 errorMessage = "Server error: The server encountered an issue. Please try again later.";
1099 569 }
1100 570 }
1101 571
1102 - replaceLastMessage("bot", errorMessage, '', [], botId);
572 + replaceLastMessage("bot", errorMessage);
1103 573 }
1104 574 });
1105 - }); // refreshNonceIfNeeded
1106 575 }
1107 576
1108 -function callMxChatStream(message, callback, botId) {
1109 - botId = botId || getMxChatBotId();
577 +function callMxChatStream(message, callback) {
578 + // Store the message in case we need to retry after session reset
579 + $('.mxchat-input-holder textarea').data('pending-message', message);
1110 580
1111 - // Store the message in case we need to retry after session reset
1112 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
581 + //console.log("Using streaming for message:", message);
1113 582
1114 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
583 + const currentModel = mxchatChat.model || 'gpt-4o';
1115 584 if (!isStreamingSupported(currentModel)) {
1116 - callMxChat(message, callback, botId);
585 + //console.log("Streaming not supported, falling back to regular call");
586 + callMxChat(message, callback);
1117 587 return;
1118 588 }
1119 589
1120 590 // Get page context if contextual awareness is enabled
1121 591 const pageContext = getPageContext();
592 +
593 + // Get bot ID
594 + const botId = getMxChatBotId();
1122 595
1123 - // Get instance for session start timestamp (used when persistence is OFF)
1124 - var instance = MxChatInstances.get(botId);
1125 -
1126 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1127 - // non-string value via String(), so passing `null` would POST the literal string "null"
1128 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1129 - // a real string even when cookies/localStorage are blocked.
1130 - var streamSessionId = MxChatInstances.ensureSession(botId);
1131 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1132 - streamSessionId = generateSessionId();
1133 - MxChatInstances.setChatSession(botId, streamSessionId);
1134 - }
1135 -
1136 - // Wait for the page-cache nonce refresh before constructing formData (which
1137 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1138 - refreshNonceIfNeeded(function() {
1139 596 const formData = new FormData();
1140 597 formData.append('action', 'mxchat_stream_chat');
1141 598 formData.append('message', message);
1142 - formData.append('session_id', streamSessionId);
599 + formData.append('session_id', getChatSession());
1143 600 formData.append('nonce', mxchatChat.nonce);
1144 601 formData.append('current_page_url', window.location.href);
1145 602 formData.append('current_page_title', document.title);
1146 - formData.append('bot_id', botId);
1147 - // Pass session start timestamp so AI context matches what user sees
1148 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
1149 -
603 + formData.append('bot_id', botId); // Include bot ID
604 +
1150 605 // Add page context if available
1151 606 if (pageContext) {
1152 607 formData.append('page_context', JSON.stringify(pageContext));
1153 608 }
@@ -1166,28 +621,23 @@
1166 621 let accumulatedContent = '';
1167 622 let testingDataReceived = false;
1168 623 let streamingStarted = false;
1169 624
1170 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1171 - // The Stop control (send button swapped in place) aborts both the read
1172 - // loop and the underlying request.
1173 - var streamControl = { controller: new AbortController(), aborted: false };
1174 - mxchatActiveStreams[botId] = streamControl;
1175 - mxchatShowStopButton(botId);
1176 -
1177 625 fetch(mxchatChat.ajax_url, {
1178 626 method: 'POST',
1179 627 body: formData,
1180 - credentials: 'same-origin',
1181 - signal: streamControl.controller.signal
628 + credentials: 'same-origin'
1182 629 })
1183 630 .then(response => {
631 + //console.log("Streaming response received:", response);
632 +
1184 633 // Store the response for potential fallback handling
1185 634 const responseClone = response.clone();
1186 -
635 +
1187 636 if (!response.ok) {
1188 637 // Try to get error details from response
1189 638 return responseClone.json().then(errorData => {
639 + //console.log("Server returned error response:", errorData);
1190 640 throw { isServerError: true, data: errorData };
1191 641 }).catch(() => {
1192 642 throw new Error('Network response was not ok');
1193 643 });
@@ -1195,21 +645,26 @@
1195 645
1196 646 // Check if response is JSON instead of streaming
1197 647 const contentType = response.headers.get('content-type');
1198 648 if (contentType && contentType.includes('application/json')) {
649 + //console.log("Received JSON response instead of stream");
1199 650 return responseClone.json().then(data => {
651 + //console.log("Processing JSON response:", data);
652 +
1200 653 // IMMEDIATE CHAT MODE UPDATE for JSON response
1201 654 if (data.chat_mode) {
1202 - updateChatModeIndicator(data.chat_mode, botId);
655 + //console.log("JSON Response: Updating chat mode to:", data.chat_mode);
656 + updateChatModeIndicator(data.chat_mode);
1203 657 }
1204 -
658 +
1205 659 // Check for testing panel
1206 660 if (window.mxchatTestPanelInstance && data.testing_data) {
661 + //console.log('Testing data found in streaming JSON response:', data.testing_data);
1207 662 window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
1208 663 }
1209 -
664 +
1210 665 // Handle the JSON response directly
1211 - handleNonStreamResponse(data, callback, botId);
666 + handleNonStreamResponse(data, callback);
1212 667 return Promise.resolve(); // Prevent further processing
1213 668 });
1214 669 }
1215 670
@@ -1220,44 +675,37 @@
1220 675
1221 676 function processStream() {
1222 677 reader.read().then(({ done, value }) => {
1223 678 if (done) {
679 + //console.log("Streaming completed, final content:", accumulatedContent);
680 +
1224 681 // If streaming completed but no content was received, try to get response as fallback
1225 682 if (!streamingStarted || !accumulatedContent) {
683 + //console.log("Stream completed with no content, checking for fallback data");
684 +
1226 685 // Try to read the response as JSON
1227 686 responseClone.text().then(text => {
1228 687 try {
1229 688 const data = JSON.parse(text);
1230 - if (data.text || data.message || data.html) {
1231 - handleNonStreamResponse(data, callback, botId);
689 + if (data.text || data.message) {
690 + handleNonStreamResponse(data, callback);
1232 691 } else {
1233 692 // No valid data, fall back to regular call
1234 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1235 - callMxChat(message, callback, botId);
693 + $('.bot-message.temporary-message').remove();
694 + callMxChat(message, callback);
1236 695 }
1237 696 } catch (e) {
1238 697 // Could not parse, fall back to regular call
1239 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1240 - callMxChat(message, callback, botId);
698 + $('.bot-message.temporary-message').remove();
699 + callMxChat(message, callback);
1241 700 }
1242 701 }).catch(() => {
1243 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1244 - callMxChat(message, callback, botId);
702 + $('.bot-message.temporary-message').remove();
703 + callMxChat(message, callback);
1245 704 });
1246 705 return;
1247 706 }
1248 -
1249 - // Re-enable chat input when stream ends with content
1250 - enableChatInput(botId);
1251 -
1252 - // Scroll the user's last message to the top now that the
1253 - // bot's full reply has rendered (gives max reading room).
1254 - var $chatBoxDone = getElement(botId, 'chat-box');
1255 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1256 - if ($lastUserMsgDone.length) {
1257 - scrollElementToTop($lastUserMsgDone, botId);
1258 - }
1259 -
707 +
1260 708 if (callback) {
1261 709 callback(accumulatedContent);
1262 710 }
1263 711 return;
@@ -1271,25 +719,17 @@
1271 719 if (line.startsWith('data: ')) {
1272 720 const data = line.substring(6);
1273 721
1274 722 if (data === '[DONE]') {
723 + //console.log("Received [DONE] signal");
724 +
1275 725 if (!accumulatedContent) {
1276 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1277 - callMxChat(message, callback, botId);
726 + //console.log("No content received before [DONE]");
727 + $('.bot-message.temporary-message').remove();
728 + callMxChat(message, callback);
1278 729 return;
1279 730 }
1280 -
1281 - // Re-enable chat input after streaming completes
1282 - enableChatInput(botId);
1283 -
1284 - // Scroll the user's last message to the top now
1285 - // that the bot's full reply has rendered.
1286 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1287 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1288 - if ($lastUserMsgStreamDone.length) {
1289 - scrollElementToTop($lastUserMsgStreamDone, botId);
1290 - }
1291 -
731 +
1292 732 if (callback) {
1293 733 callback(accumulatedContent);
1294 734 }
1295 735 return;
@@ -1296,16 +736,18 @@
1296 736 }
1297 737
1298 738 try {
1299 739 const json = JSON.parse(data);
1300 -
740 +
1301 741 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
1302 742 if (json.chat_mode) {
1303 - updateChatModeIndicator(json.chat_mode, botId);
743 + //console.log("Stream: Updating chat mode immediately to:", json.chat_mode);
744 + updateChatModeIndicator(json.chat_mode);
1304 745 }
1305 -
746 +
1306 747 // Handle testing data
1307 748 if (json.testing_data && !testingDataReceived) {
749 + //console.log('Testing data received in stream:', json.testing_data);
1308 750 if (window.mxchatTestPanelInstance) {
1309 751 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
1310 752 testingDataReceived = true;
1311 753 }
@@ -1313,27 +755,26 @@
1313 755 // Handle content streaming
1314 756 else if (json.content) {
1315 757 streamingStarted = true;
1316 758 accumulatedContent += json.content;
1317 - updateStreamingMessage(accumulatedContent, botId);
1318 - }
759 + updateStreamingMessage(accumulatedContent);
760 + }
1319 761 // Handle complete response in stream (fallback response)
1320 - else if (json.text || json.message || json.html) {
1321 - handleNonStreamResponse(json, callback, botId);
762 + else if (json.text || json.message) {
763 + //console.log("Received complete response in stream:", json);
764 + handleNonStreamResponse(json, callback);
1322 765 return;
1323 766 }
1324 - // Handle errors
767 + // Handle errors - UPDATED to properly display API errors
1325 768 else if (json.error) {
769 + //console.error("Streaming error:", json.error);
1326 770
1327 771 // Get error message from various possible fields
1328 772 let errorMessage = json.error_message || json.message || json.text ||
1329 773 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1330 774
1331 - // Re-enable chat input on error
1332 - enableChatInput(botId);
1333 -
1334 775 // Display the error directly in the chat
1335 - replaceLastMessage("bot", errorMessage, '', [], botId);
776 + replaceLastMessage("bot", errorMessage);
1336 777
1337 778 if (callback) {
1338 779 callback(errorMessage);
1339 780 }
@@ -1339,9 +780,9 @@
1339 780 }
1340 781 return;
1341 782 }
1342 783 } catch (e) {
1343 - // SSE data parsing error - silently continue
784 + //console.error('Error parsing SSE data:', e, 'Data:', data);
1344 785 }
1345 786 }
1346 787 }
1347 788
@@ -1346,11 +787,11 @@
1346 787 }
1347 788
1348 789 processStream();
1349 790 }).catch(streamError => {
1350 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1351 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1352 - callMxChat(message, callback, botId);
791 + //console.error('Error reading stream:', streamError);
792 + $('.bot-message.temporary-message').remove();
793 + callMxChat(message, callback);
1353 794 });
1354 795 }
1355 796
1356 797 processStream();
@@ -1355,42 +796,48 @@
1355 796
1356 797 processStream();
1357 798 })
1358 799 .catch(error => {
1359 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
800 + //console.log('Streaming failed:', error);
801 +
1360 802 // Check if we have server error data with chat mode
1361 803 if (error && error.isServerError && error.data) {
804 + //console.log('Using server error response data');
805 +
1362 806 // Check for chat mode in error data
1363 807 if (error.data.chat_mode) {
1364 - updateChatModeIndicator(error.data.chat_mode, botId);
808 + //console.log("Error Response: Updating chat mode to:", error.data.chat_mode);
809 + updateChatModeIndicator(error.data.chat_mode);
1365 810 }
1366 -
1367 - handleNonStreamResponse(error.data, callback, botId);
811 +
812 + handleNonStreamResponse(error.data, callback);
1368 813 } else {
1369 814 // Only fall back to regular call if we don't have any response data
1370 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1371 - callMxChat(message, callback, botId);
815 + //console.log('No response data available, falling back to regular call');
816 + $('.bot-message.temporary-message').remove();
817 + callMxChat(message, callback);
1372 818 }
1373 819 });
1374 - }); // refreshNonceIfNeeded
1375 820 }
1376 821
1377 -// Helper function to handle non-streaming responses
1378 -function handleNonStreamResponse(data, callback, botId) {
1379 - botId = botId || 'default';
822 +// Helper function to handle non-streaming responses (updated to include immediate chat mode handling)
823 +function handleNonStreamResponse(data, callback) {
824 + //console.log("Handling non-stream response:", data);
1380 825
1381 826 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1382 827 if (data.chat_mode) {
1383 - updateChatModeIndicator(data.chat_mode, botId);
828 + //console.log("Non-Stream: Updating chat mode immediately to:", data.chat_mode);
829 + updateChatModeIndicator(data.chat_mode);
1384 830 }
1385 831
1386 832 // Also check in data property if response is wrapped
1387 833 if (data.data && data.data.chat_mode) {
1388 - updateChatModeIndicator(data.data.chat_mode, botId);
834 + //console.log("Non-Stream: Updating chat mode from data to:", data.data.chat_mode);
835 + updateChatModeIndicator(data.data.chat_mode);
1389 836 }
1390 837
1391 - // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1392 - // This prevents a visual gap between thinking dots disappearing and content appearing
838 + // Remove temporary message
839 + $('.bot-message.temporary-message').remove();
1393 840
1394 841 // SECURITY FIX: Check for errors FIRST
1395 842 if (data.success === false || (data.data && data.data.error_message)) {
1396 843 let errorMessage = "";
@@ -1411,20 +858,25 @@
1411 858 errorMessage = "An error occurred. Please try again or contact support.";
1412 859 }
1413 860
1414 861 // Handle session reset action (IP changed, session expired, etc.)
1415 - // Silent reset — keep chat UI intact, just get a new session and retry
1416 862 if (data.data && data.data.action === 'reset_session') {
1417 - MxChatInstances.silentResetSession(botId);
1418 - // Re-send the original message with the new session (user message is already displayed)
1419 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
863 + // Clear the old session and generate a new one
864 + resetChatSession();
865 + // Re-send the original message with the new session
866 + var originalMessage = $('.mxchat-input-holder textarea').data('pending-message');
1420 867 if (originalMessage) {
1421 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1422 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
868 + $('.mxchat-input-holder textarea').data('pending-message', null);
869 + // Re-add the user message and thinking indicator
870 + appendMessage("user", originalMessage);
871 + appendThinkingMessage();
872 + scrollToBottom();
873 + // Determine whether to use streaming
874 + const currentModel = mxchatChat.model || 'gpt-4o';
1423 875 if (shouldUseStreaming(currentModel)) {
1424 - callMxChatStream(originalMessage, callback, botId);
876 + callMxChatStream(originalMessage, callback);
1425 877 } else {
1426 - callMxChat(originalMessage, callback, botId);
878 + callMxChat(originalMessage, callback);
1427 879 }
1428 880 }
1429 881 return;
1430 882 }
@@ -1434,9 +886,9 @@
1434 886 if (mxchatChat.is_admin) {
1435 887 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1436 888 }
1437 889
1438 - replaceLastMessage("bot", displayMessage, '', [], botId);
890 + replaceLastMessage("bot", displayMessage);
1439 891
1440 892 if (callback) {
1441 893 callback('');
1442 894 }
@@ -1442,22 +894,8 @@
1442 894 }
1443 895 return; // Exit early for errors
1444 896 }
1445 897
1446 - // Check for live agent response
1447 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1448 - removeThinkingDots(botId);
1449 - // Also remove any leftover bot-message that lost its temporary-message class
1450 - var $chatBox = getElement(botId, 'chat-box');
1451 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1452 - updateChatModeIndicator('agent', botId);
1453 - enableChatInput(botId);
1454 - if (callback) {
1455 - callback('');
1456 - }
1457 - return;
1458 - }
1459 -
1460 898 // Handle different response formats
1461 899 if (data.text || data.html || data.message) {
1462 900
1463 901 // Apply response hooks
@@ -1469,23 +907,22 @@
1469 907 }
1470 908
1471 909 // Display the response
1472 910 if (data.text && data.html) {
1473 - replaceLastMessage("bot", data.text, data.html, [], botId);
911 + replaceLastMessage("bot", data.text, data.html);
1474 912 } else if (data.text) {
1475 - replaceLastMessage("bot", data.text, '', [], botId);
913 + replaceLastMessage("bot", data.text);
1476 914 } else if (data.html) {
1477 - replaceLastMessage("bot", "", data.html, [], botId);
915 + replaceLastMessage("bot", "", data.html);
1478 916 } else if (data.message) {
1479 - replaceLastMessage("bot", data.message, '', [], botId);
917 + replaceLastMessage("bot", data.message);
1480 918 }
1481 919 }
1482 920
1483 921 // Handle other response properties
1484 922 if (data.data && data.data.filename) {
1485 - showActivePdf(data.data.filename, botId);
1486 - var instance = MxChatInstances.get(botId);
1487 - instance.activePdfFile = data.data.filename;
923 + showActivePdf(data.data.filename);
924 + activePdfFile = data.data.filename;
1488 925 }
1489 926
1490 927 if (data.redirect_url) {
1491 928 setTimeout(() => {
@@ -1492,33 +929,31 @@
1492 929 window.location.href = data.redirect_url;
1493 930 }, 1500);
1494 931 }
1495 932
1496 - // Ensure chat input is re-enabled (safety net for edge cases)
1497 - enableChatInput(botId);
1498 -
1499 933 if (callback) {
1500 934 callback(data.text || data.message || '');
1501 935 }
1502 936 }
1503 -
1504 937 // Enhanced updateChatModeIndicator function for immediate DOM updates
1505 -function updateChatModeIndicator(mode, botId) {
1506 - botId = botId || 'default';
1507 - const indicator = getElementDOM(botId, 'chat-mode-indicator');
938 +function updateChatModeIndicator(mode) {
939 + //console.log("updateChatModeIndicator called with mode:", mode);
940 + const indicator = document.getElementById('chat-mode-indicator');
1508 941 if (indicator) {
1509 942 const oldText = indicator.textContent;
1510 -
943 +
1511 944 if (mode === 'agent') {
1512 945 indicator.textContent = 'Live Agent';
1513 - startPolling(botId);
946 + startPolling();
947 + //console.log("Set indicator to Live Agent");
1514 948 } else {
1515 949 // Everything else is AI mode
1516 950 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1517 951 indicator.textContent = customAiText;
1518 - stopPolling(botId);
952 + stopPolling();
953 + //console.log("Set indicator to AI Agent:", customAiText);
1519 954 }
1520 -
955 +
1521 956 // Force immediate DOM update and reflow
1522 957 if (oldText !== indicator.textContent) {
1523 958 // Force a reflow to ensure the change is visible immediately
1524 959 indicator.style.display = 'none';
@@ -1523,36 +958,88 @@
1523 958 // Force a reflow to ensure the change is visible immediately
1524 959 indicator.style.display = 'none';
1525 960 indicator.offsetHeight; // Trigger reflow
1526 961 indicator.style.display = '';
1527 -
962 +
1528 963 // Double-check after a brief moment to ensure the change stuck
1529 964 setTimeout(() => {
965 + //console.log("Final indicator text:", indicator.textContent);
1530 966 if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
967 + //console.warn("Mode indicator update failed, forcing update");
1531 968 indicator.textContent = 'Live Agent';
1532 969 } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
970 + //console.warn("Mode indicator stuck on Live Agent, forcing AI update");
1533 971 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1534 972 indicator.textContent = customAiText;
1535 973 }
1536 974 }, 50);
1537 975 }
976 + } else {
977 + //console.error("Chat mode indicator element not found!");
1538 978 }
1539 979 }
1540 980
981 +// Helper function to handle non-streaming responses (updated to include chat mode handling)
982 +function handleNonStreamResponse(data, callback) {
983 + //console.log("Handling non-stream response:", data);
984 +
985 + // Remove temporary message
986 + $('.bot-message.temporary-message').remove();
987 +
988 + // Handle different response formats
989 + if (data.text || data.html || data.message) {
990 +
991 + // Apply response hooks
992 + if (data.text && typeof customMxChatFilter === 'function') {
993 + data.text = customMxChatFilter(data.text, "response");
994 + }
995 + if (data.message && typeof customMxChatFilter === 'function') {
996 + data.message = customMxChatFilter(data.message, "response");
997 + }
998 +
999 + // Handle chat mode updates for streaming responses too
1000 + handleChatModeUpdates(data, data.text || data.message);
1001 +
1002 + // Display the response
1003 + if (data.text && data.html) {
1004 + replaceLastMessage("bot", data.text, data.html);
1005 + } else if (data.text) {
1006 + replaceLastMessage("bot", data.text);
1007 + } else if (data.html) {
1008 + replaceLastMessage("bot", "", data.html);
1009 + } else if (data.message) {
1010 + replaceLastMessage("bot", data.message);
1011 + }
1012 + }
1013 +
1014 + // Handle other response properties
1015 + if (data.data && data.data.filename) {
1016 + showActivePdf(data.data.filename);
1017 + activePdfFile = data.data.filename;
1018 + }
1019 +
1020 + if (data.redirect_url) {
1021 + setTimeout(() => {
1022 + window.location.href = data.redirect_url;
1023 + }, 1500);
1024 + }
1025 +
1026 + if (callback) {
1027 + callback(data.text || data.message || '');
1028 + }
1029 +}
1030 +
1541 1031 // Function to update message during streaming
1542 -function updateStreamingMessage(content, botId) {
1543 - botId = botId || 'default';
1544 -
1032 +function updateStreamingMessage(content) {
1545 1033 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1546 1034 if (typeof customMxChatFilter === 'function') {
1547 1035 content = customMxChatFilter(content, "response");
1548 1036 }
1549 -
1037 +
1550 1038 const formattedContent = linkify(content);
1551 1039
1552 - // Find the temporary message in this bot's chat box
1553 - var $chatBox = getElement(botId, 'chat-box');
1554 - const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
1040 + // Find the temporary message
1041 + const tempMessage = $('.bot-message.temporary-message').last();
1555 1042
1556 1043 if (tempMessage.length) {
1557 1044 // Update existing message
1558 1045 tempMessage.html(formattedContent);
@@ -1557,9 +1044,9 @@
1557 1044 // Update existing message
1558 1045 tempMessage.html(formattedContent);
1559 1046 } else {
1560 1047 // Create new temporary message if it doesn't exist
1561 - appendMessage("bot", content, '', [], true, botId);
1048 + appendMessage("bot", content, '', [], true);
1562 1049 }
1563 1050 }
1564 1051
1565 1052 function isStreamingSupported(model) {
@@ -1577,404 +1064,23 @@
1577 1064
1578 1065 return isSupported;
1579 1066 }
1580 1067
1581 -// Update the event handlers to use the correct function names (using event delegation)
1582 -// Use class-based selectors for multi-instance support
1583 -$(document).on('click', '.send-button', function() {
1584 - var botId = getBotIdFromElement(this);
1585 - // While a response is streaming the button is a Stop control.
1586 - if (this.classList.contains('mxchat-stop-mode')) {
1587 - // Same click that just started this stream (an add-on's direct handler
1588 - // ran before this delegated one) — not a Stop press. See
1589 - // mxchatShowStopButton for the full story (plan-4bba64).
1590 - if (this.__mxchatStopJustShown) return;
1591 - mxchatStopStreaming(botId);
1592 - return;
1593 - }
1594 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1595 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1596 - disableChatInput(botId);
1597 - }
1598 - sendMessage(botId);
1068 +// Update the event handlers to use the correct function names
1069 +$('#send-button').off('click').on('click', function() {
1070 + sendMessage(); // Use the updated sendMessage function
1599 1071 });
1600 1072
1601 -// Override enter key handler (using event delegation)
1602 -$(document).on('keypress', '.chat-input', function(e) {
1073 +// Override enter key handler
1074 +$('#chat-input').off('keypress').on('keypress', function(e) {
1603 1075 if (e.which == 13 && !e.shiftKey) {
1604 1076 e.preventDefault();
1605 - var botId = getBotIdFromElement(this);
1606 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1607 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1608 - disableChatInput(botId);
1609 - }
1610 - sendMessage(botId);
1077 + sendMessage(); // Use the updated sendMessage function
1611 1078 }
1612 1079 });
1613 1080
1614 -// Chat input character counter + soft limit feedback (plan 7091a2).
1615 -// Language-neutral: numbers + color only, no translatable strings. The counter
1616 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1617 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1618 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1619 -function mxchatUpdateCharCounter(inputEl) {
1620 - if (!inputEl || !inputEl.closest) return;
1621 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1622 - var container = inputEl.closest('.input-container');
1623 - if (!container || !max || max <= 0) return;
1624 - var counter = container.querySelector('.mxchat-char-counter');
1625 - if (!counter) return;
1626 - var len = inputEl.value.length;
1627 - var ratio = len / max;
1628 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1629 - var cur = counter.querySelector('.mxchat-char-counter-current');
1630 - if (cur) cur.textContent = len;
1631 - var warn = ratio >= nearThreshold && len < max;
1632 - var full = len >= max;
1633 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1634 - counter.classList.toggle('is-warn', warn);
1635 - counter.classList.toggle('is-full', full);
1636 - container.classList.toggle('mxchat-input-near-limit', warn);
1637 - container.classList.toggle('mxchat-input-at-limit', full);
1638 -}
1639 -
1640 -function mxchatBumpInput(inputEl) {
1641 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1642 - if (!container) return;
1643 - container.classList.remove('mxchat-input-bump');
1644 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1645 - container.classList.add('mxchat-input-bump');
1646 - clearTimeout($(container).data('mxchatBumpTimeout'));
1647 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1648 - $(container).data('mxchatBumpTimeout', t);
1649 -}
1650 -
1651 -// Live counter update on every input.
1652 -$(document).on('input', '.chat-input', function() {
1653 - mxchatUpdateCharCounter(this);
1654 -});
1655 -
1656 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1657 -// rejected at the cap (maxlength silently swallows it otherwise).
1658 -$(document).on('keydown', '.chat-input', function(e) {
1659 - var max = parseInt(this.getAttribute('maxlength'), 10);
1660 - if (!max || max <= 0 || this.value.length < max) return;
1661 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1662 - // A single printable char with no selection to overwrite WILL be rejected.
1663 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1664 - mxchatBumpInput(this);
1665 - }
1666 -});
1667 -
1668 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1669 -$(document).on('paste', '.chat-input', function() {
1670 - var el = this;
1671 - var max = parseInt(el.getAttribute('maxlength'), 10);
1672 - if (!max || max <= 0) return;
1673 - setTimeout(function() {
1674 - mxchatUpdateCharCounter(el);
1675 - if (el.value.length >= max) mxchatBumpInput(el);
1676 - }, 0);
1677 -});
1678 -
1679 -// Builds the list of overflow-menu items for a given bot.
1680 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1681 -function mxchatGetHeaderMenuItems(botId) {
1682 - var items = [];
1683 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1684 -
1685 - // The `print_button_*` keys still gate this item for back-compat with
1686 - // existing user options. The action is now a transcript download, not print.
1687 - if (settings.print_button_enabled === 'on') {
1688 - items.push({
1689 - id: 'download-transcript',
1690 - label: settings.print_button_label || 'Download Transcript',
1691 - 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>',
1692 - action: function() {
1693 - mxchatDownloadTranscript(botId);
1694 - }
1695 - });
1696 - }
1697 -
1698 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1699 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1700 - // without the site owner disabling chat persistence globally. Default OFF;
1701 - // gated by the reset_chat_enabled option. plan ac2e81.
1702 - if (settings.reset_chat_enabled === 'on') {
1703 - items.push({
1704 - id: 'reset-chat',
1705 - label: settings.reset_chat_label || 'Start new chat',
1706 - 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"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
1707 - action: function() {
1708 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1709 - if (window.confirm(confirmMsg)) {
1710 - MxChatInstances.resetChatSession(botId);
1711 - }
1712 - }
1713 - });
1714 - }
1715 -
1716 - return items;
1717 -}
1718 -
1719 -// Builds a clean markdown transcript of the current conversation and triggers
1720 -// a file download. Used by the "Download Transcript" menu item.
1721 -function mxchatDownloadTranscript(botId) {
1722 - var $chatBox = getElement(botId, 'chat-box');
1723 - if (!$chatBox || !$chatBox.length) return;
1724 -
1725 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1726 - var headerTitle = settings.print_header_title || 'Chat transcript';
1727 - var now = new Date();
1728 - var stamp = now.toLocaleString();
1729 -
1730 - var lines = [];
1731 - lines.push('# ' + headerTitle);
1732 - lines.push('');
1733 - lines.push('Exported: ' + stamp);
1734 - lines.push('');
1735 - lines.push('---');
1736 - lines.push('');
1737 -
1738 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1739 - var $msg = $(this);
1740 - // Skip thinking placeholders and any in-flight temporary messages.
1741 - if ($msg.find('.thinking-dots').length) return;
1742 - if ($msg.hasClass('temporary-message')) return;
1743 -
1744 - var sender;
1745 - if ($msg.hasClass('user-message')) sender = 'User';
1746 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1747 - else sender = 'AI Agent';
1748 -
1749 - // Strip interactive UI from the cloned message so we get the conversation text.
1750 - var $clone = $msg.clone();
1751 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1752 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1753 - if (!text) return;
1754 -
1755 - lines.push('**' + sender + '**');
1756 - lines.push('');
1757 - lines.push(text);
1758 - lines.push('');
1759 - });
1760 -
1761 - var content = lines.join('\n');
1762 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1763 - var fname = 'mxchat-transcript-' + iso + '.md';
1764 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1765 - var url = URL.createObjectURL(blob);
1766 - var a = document.createElement('a');
1767 - a.href = url;
1768 - a.download = fname;
1769 - a.style.display = 'none';
1770 - document.body.appendChild(a);
1771 - a.click();
1772 - setTimeout(function() {
1773 - if (a.parentNode) a.parentNode.removeChild(a);
1774 - URL.revokeObjectURL(url);
1775 - }, 100);
1776 -}
1777 -
1778 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1779 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1780 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1781 -function mxchatSyncMenuColors(botId, $wrap) {
1782 - if (!$wrap || !$wrap.length) return;
1783 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1784 - if (!$bot.length) return;
1785 - var cs = window.getComputedStyle($bot[0]);
1786 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1787 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1788 - }
1789 - // Bot text color usually lives on a child div, not .bot-message itself.
1790 - var $textChild = $bot.find('[style*="color"]').first();
1791 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1792 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1793 -}
1794 -
1795 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1796 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1797 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1798 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1799 -// a rebuild before init (never happens, but harmless) just skips it.
1800 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1801 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1802 - var $menu = $wrap.find('.mxchat-header-menu');
1803 - var items = mxchatGetHeaderMenuItems(botId);
1804 -
1805 - $menu.empty();
1806 -
1807 - if (!items.length) {
1808 - $trigger.hide();
1809 - $menu.hide();
1810 - return;
1811 - }
1812 -
1813 - // Clear any inline display:none a previous zero-item render left behind —
1814 - // open/close visibility is governed by the hidden prop + is-open class.
1815 - $trigger.css('display', '');
1816 - $menu.css('display', '');
1817 -
1818 - items.forEach(function(item, idx) {
1819 - var $btn = $('<button>', {
1820 - type: 'button',
1821 - 'class': 'mxchat-menu-item',
1822 - 'role': 'menuitem',
1823 - 'tabindex': '-1',
1824 - 'data-menu-id': item.id,
1825 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1826 - '<span class="mxchat-menu-item-label"></span>'
1827 - });
1828 - $btn.find('.mxchat-menu-item-label').text(item.label);
1829 - $btn.on('click', function(e) {
1830 - e.preventDefault();
1831 - e.stopPropagation();
1832 - if (closeMenuFn) closeMenuFn();
1833 - try { item.action(); } catch (err) { /* no-op */ }
1834 - });
1835 - $menu.append($btn);
1836 - });
1837 -}
1838 -
1839 -// Re-render every menu on the page after a dynamic-settings merge
1840 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1841 -// swapping items under the user mid-interaction yanks focus — and the
1842 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1843 -function mxchatRebuildHeaderMenus() {
1844 - $('.mxchat-header-menu-wrap').each(function() {
1845 - var $wrap = $(this);
1846 - var botId = $wrap.data('bot-id');
1847 - if (!botId) return;
1848 - if (!$wrap.data('mxchatMenuReady')) {
1849 - mxchatInitHeaderMenu(botId);
1850 - return;
1851 - }
1852 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1853 - $wrap.data('mxchatMenuRebuildPending', true);
1854 - return;
1855 - }
1856 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1857 - });
1858 -}
1859 -
1860 -// One-time per-widget init: renders menu items, wires open/close,
1861 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1862 -// trigger. Wiring happens even when there are zero items at init, so a
1863 -// later dynamic-settings rebuild that adds items has a working trigger.
1864 -function mxchatInitHeaderMenu(botId) {
1865 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1866 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1867 -
1868 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1869 - var $menu = $wrap.find('.mxchat-header-menu');
1870 -
1871 - // Initial color sync — covers normal page load.
1872 - mxchatSyncMenuColors(botId, $wrap);
1873 -
1874 - function openMenu() {
1875 - // Re-sync each open in case the active theme changed since init.
1876 - mxchatSyncMenuColors(botId, $wrap);
1877 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1878 - $trigger.attr('aria-expanded', 'true');
1879 - // Focus the first item for keyboard users
1880 - setTimeout(function() {
1881 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1882 - }, 0);
1883 - }
1884 - function closeMenu(returnFocus) {
1885 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1886 - $trigger.attr('aria-expanded', 'false');
1887 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1888 - if (returnFocus) $trigger.trigger('focus');
1889 - // A dynamic-settings rebuild that arrived while the menu was open
1890 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1891 - if ($wrap.data('mxchatMenuRebuildPending')) {
1892 - $wrap.removeData('mxchatMenuRebuildPending');
1893 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1894 - }
1895 - }
1896 -
1897 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1898 - // click-to-collapse handler does not fire.
1899 - $trigger.on('click', function(e) {
1900 - e.preventDefault();
1901 - e.stopPropagation();
1902 - if ($menu.hasClass('is-open')) closeMenu();
1903 - else openMenu();
1904 - });
1905 -
1906 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1907 - $menu.on('click', function(e) {
1908 - e.stopPropagation();
1909 - });
1910 -
1911 - // Outside click closes the menu.
1912 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1913 - if (!$menu.hasClass('is-open')) return;
1914 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1915 - closeMenu();
1916 - });
1917 -
1918 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1919 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1920 - var $items = $menu.find('.mxchat-menu-item');
1921 - var idx = $items.index(this);
1922 - if (e.key === 'Escape') {
1923 - e.preventDefault();
1924 - closeMenu(true);
1925 - } else if (e.key === 'ArrowDown') {
1926 - e.preventDefault();
1927 - var $next = $items.eq((idx + 1) % $items.length);
1928 - $items.attr('tabindex', '-1');
1929 - $next.attr('tabindex', '0').trigger('focus');
1930 - } else if (e.key === 'ArrowUp') {
1931 - e.preventDefault();
1932 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1933 - $items.attr('tabindex', '-1');
1934 - $prev.attr('tabindex', '0').trigger('focus');
1935 - } else if (e.key === 'Enter' || e.key === ' ') {
1936 - e.preventDefault();
1937 - $(this).trigger('click');
1938 - }
1939 - });
1940 - $trigger.on('keydown', function(e) {
1941 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1942 - e.preventDefault();
1943 - closeMenu(true);
1944 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1945 - e.preventDefault();
1946 - openMenu();
1947 - }
1948 - });
1949 -
1950 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1951 - // then do the initial item render.
1952 - $wrap.data('mxchatMenuClose', closeMenu);
1953 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1954 -
1955 - $wrap.data('mxchatMenuReady', true);
1956 -}
1957 -
1958 -// Initialize header menus for every rendered widget on DOM ready.
1959 -$(function() {
1960 - $('.mxchat-header-menu-wrap').each(function() {
1961 - var botId = $(this).data('bot-id');
1962 - if (botId) mxchatInitHeaderMenu(botId);
1963 - });
1964 -
1965 - // Embedded (non-floating) widgets are open from the moment the page
1966 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1967 - // widgets refresh on first launcher open instead.
1968 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1969 - return !$(this).closest('.floating-chatbot').length;
1970 - }).length > 0;
1971 - if (hasEmbeddedWidget) {
1972 - mxchatRefreshDynamicSettings();
1973 - }
1974 -});
1975 -
1976 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1081 +
1082 +function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
1977 1083 try {
1978 1084 // Determine styles based on sender type
1979 1085 let messageClass, bgColor, fontColor;
1980 1086
@@ -1995,29 +1101,25 @@
1995 1101 }
1996 1102
1997 1103 const messageDiv = $('<div>')
1998 1104 .addClass(messageClass)
1999 - .attr('dir', 'auto');
2000 -
2001 - // Only apply inline colors if AI theme is not active (let CSS handle it)
2002 - var skipColors = shouldSkipInlineColors(botId);
2003 - if (skipColors) {
2004 - messageDiv.css({
2005 - 'margin-bottom': '1em'
2006 - });
2007 - } else {
2008 - messageDiv.css({
1105 + .attr('dir', 'auto')
1106 + .css({
2009 1107 'background': bgColor,
2010 1108 'color': fontColor,
2011 1109 'margin-bottom': '1em'
2012 1110 });
1111 +
1112 + // Process the message content based on sender
1113 + let fullMessage;
1114 + if (sender === "user") {
1115 + // For user messages, apply linkify after sanitization
1116 + fullMessage = linkify(messageText);
1117 + } else {
1118 + // For bot/agent messages, preserve HTML
1119 + fullMessage = messageText;
2013 1120 }
2014 1121
2015 - // Process the message content - always run linkify to convert markdown
2016 - // links and format text. linkify() handles existing HTML safely via
2017 - // negative lookaheads that skip URLs already inside <a> tags.
2018 - let fullMessage = linkify(messageText);
2019 -
2020 1122 // Add images if provided
2021 1123 if (images && images.length > 0) {
2022 1124 fullMessage += '<div class="image-gallery" dir="auto">';
2023 1125 images.forEach(img => {
@@ -2023,9 +1125,9 @@
2023 1125 images.forEach(img => {
2024 1126 const safeTitle = sanitizeUserInput(img.title);
2025 1127 const safeUrl = encodeURI(img.image_url);
2026 1128 const safeThumbnail = encodeURI(img.thumbnail_url);
2027 -
1129 +
2028 1130 fullMessage += `
2029 1131 <div style="margin-bottom: 10px;">
2030 1132 <strong>${safeTitle}</strong><br>
2031 1133 <a href="${safeUrl}" target="_blank">
@@ -2037,14 +1139,9 @@
2037 1139 }
2038 1140
2039 1141 // Append HTML content if provided
2040 1142 if (messageHtml && sender !== "user") {
2041 - // Only add line breaks if there's actual text content before the HTML
2042 - if (fullMessage && fullMessage.trim()) {
2043 - fullMessage += '<br><br>' + messageHtml;
2044 - } else {
2045 - fullMessage = messageHtml;
2046 - }
1143 + fullMessage += '<br><br>' + messageHtml;
2047 1144 }
2048 1145
2049 1146 messageDiv.html(fullMessage);
2050 1147
@@ -2051,66 +1148,58 @@
2051 1148 if (isTemporary) {
2052 1149 messageDiv.addClass('temporary-message');
2053 1150 }
2054 1151
2055 - // Append to the correct chatbot instance's chat-box
2056 - var $chatBox = getElement(botId, 'chat-box');
2057 - messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1152 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
2058 1153 // FIXED: Use event delegation for link tracking
2059 1154 if (sender === "bot" || sender === "agent") {
2060 - attachLinkTracking(messageDiv, messageText, botId);
1155 + attachLinkTracking(messageDiv, messageText);
2061 1156 }
2062 -
1157 +
2063 1158 if (sender === "bot") {
2064 - const lastUserMessage = $chatBox.find('.user-message').last();
1159 + const lastUserMessage = $('#chat-box').find('.user-message').last();
2065 1160 if (lastUserMessage.length) {
2066 - scrollElementToTop(lastUserMessage, botId);
1161 + scrollElementToTop(lastUserMessage);
2067 1162 }
2068 1163 }
2069 -
2070 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2071 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2072 - }
2073 1164 });
2074 1165
2075 1166 if (messageText.id) {
2076 - var instance = MxChatInstances.get(botId);
2077 - instance.lastSeenMessageId = messageText.id;
2078 - hideNotification(botId);
1167 + lastSeenMessageId = messageText.id;
1168 + hideNotification();
2079 1169 }
2080 1170 } catch (error) {
2081 - // Error rendering message - silently continue
1171 + //console.error("Error rendering message:", error);
2082 1172 }
2083 1173 }
2084 1174
2085 1175 // Helper function to attach link tracking with proper event handling
2086 -function attachLinkTracking(messageDiv, messageText, botId) {
2087 - botId = botId || 'default';
1176 +function attachLinkTracking(messageDiv, messageText) {
2088 1177 // Use a slight delay to ensure DOM is ready
2089 1178 setTimeout(function() {
2090 1179 const links = messageDiv.find('a[href]').not('[data-tracked]');
2091 -
1180 +
2092 1181 links.each(function() {
2093 1182 const $link = $(this);
2094 1183 const originalHref = $link.attr('href');
2095 -
1184 +
2096 1185 // Mark as tracked to avoid duplicate handlers
2097 1186 $link.attr('data-tracked', 'true');
2098 -
1187 +
2099 1188 // Only track external URLs
2100 1189 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2101 1190 // Remove any existing click handlers first
2102 1191 $link.off('click.tracking');
2103 -
1192 +
2104 1193 // Add new click handler with namespace
2105 1194 $link.on('click.tracking', function(e) {
2106 1195 e.preventDefault();
2107 1196 e.stopPropagation();
2108 -
2109 - const messageContext = typeof messageText === 'string'
2110 - ? messageText.substring(0, 200)
1197 +
1198 + const messageContext = typeof messageText === 'string'
1199 + ? messageText.substring(0, 200)
2111 1200 : '';
2112 -
1201 +
2113 1202 // Track the click
2114 1203 $.ajax({
2115 1204 url: mxchatChat.ajax_url,
2116 1205 type: 'POST',
@@ -2115,9 +1204,9 @@
2115 1204 url: mxchatChat.ajax_url,
2116 1205 type: 'POST',
2117 1206 data: {
2118 1207 action: 'mxchat_track_url_click',
2119 - session_id: getChatSession(botId),
1208 + session_id: getChatSession(),
2120 1209 url: originalHref,
2121 1210 message_context: messageContext,
2122 1211 nonce: mxchatChat.nonce
2123 1212 },
@@ -2129,9 +1218,9 @@
2129 1218 window.location.href = originalHref;
2130 1219 }
2131 1220 }
2132 1221 });
2133 -
1222 +
2134 1223 return false; // Extra insurance to prevent default
2135 1224 });
2136 1225 }
2137 1226 });
@@ -2137,12 +1226,11 @@
2137 1226 });
2138 1227 }, 100); // Small delay to ensure DOM is ready
2139 1228 }
2140 1229
2141 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1230 +function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
2142 1231 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
2143 - var $chatBox = getElement(botId, 'chat-box');
2144 - var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1232 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
2145 1233
2146 1234 // Determine styles
2147 1235 let bgColor, fontColor;
2148 1236 if (sender === "user") {
@@ -2155,13 +1243,27 @@
2155 1243 bgColor = botMessageBgColor;
2156 1244 fontColor = botMessageFontColor;
2157 1245 }
2158 1246
2159 - // Always run linkify to convert markdown links and format text.
2160 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2161 - // to avoid double-processing URLs that are already inside <a> tags).
2162 - var fullMessage = linkify(responseText);
2163 -
1247 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1248 + // This prevents double-processing of URLs that are already formatted as HTML
1249 + var fullMessage;
1250 + if (sender === "user") {
1251 + // Always linkify user messages (they're plain text)
1252 + fullMessage = linkify(responseText);
1253 + } else {
1254 + // For bot/agent messages, check if HTML already exists
1255 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1256 + responseText.includes('<img') || responseText.includes('<div') ||
1257 + responseText.includes('<p>') || responseText.includes('<br>')) {
1258 + // Response already has HTML, don't process it
1259 + fullMessage = responseText;
1260 + } else {
1261 + // Plain text response, apply linkify
1262 + fullMessage = linkify(responseText);
1263 + }
1264 + }
1265 +
2164 1266 if (responseHtml) {
2165 1267 // Only add line breaks if there's actual text content before the HTML
2166 1268 if (fullMessage && fullMessage.trim()) {
2167 1269 fullMessage += '<br><br>' + responseHtml;
@@ -2184,96 +1286,67 @@
2184 1286 fullMessage += '</div>';
2185 1287 }
2186 1288
2187 1289 if (lastMessageDiv.length) {
2188 - // Replace content immediately to prevent visual gap between thinking dots and response
2189 - lastMessageDiv
2190 - .html(fullMessage)
2191 - .removeClass('bot-message user-message temporary-message')
2192 - .addClass(messageClass)
2193 - .attr('dir', 'auto');
2194 -
2195 - // Only apply inline colors if AI theme is not active (let CSS handle it)
2196 - var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
2197 - if (!skipColors) {
2198 - lastMessageDiv.css({
2199 - 'background-color': bgColor,
2200 - 'color': fontColor,
2201 - });
2202 - }
2203 -
2204 - // Handle link tracking and scroll
2205 - if (sender === "bot" || sender === "agent") {
2206 - attachLinkTracking(lastMessageDiv, responseText, botId);
2207 -
2208 - const lastUserMessage = $chatBox.find('.user-message').last();
2209 - if (lastUserMessage.length) {
2210 - scrollElementToTop(lastUserMessage, botId);
2211 - }
2212 - // Show notification if chat is hidden
2213 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2214 - if ($floatingChatbot.hasClass('hidden')) {
2215 - showNotification(botId);
2216 - }
2217 - }
2218 -
2219 - // Re-enable chat input after response is displayed
2220 - enableChatInput(botId);
2221 -
2222 - if (sender === "bot" || sender === "agent") {
2223 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2224 - }
1290 + lastMessageDiv.fadeOut(200, function() {
1291 + $(this)
1292 + .html(fullMessage)
1293 + .removeClass('bot-message user-message')
1294 + .addClass(messageClass)
1295 + .attr('dir', 'auto')
1296 + .css({
1297 + 'background-color': bgColor,
1298 + 'color': fontColor,
1299 + })
1300 + .removeClass('temporary-message')
1301 + .fadeIn(200, function() {
1302 + // FIXED: Use the helper function for link tracking
1303 + if (sender === "bot" || sender === "agent") {
1304 + attachLinkTracking($(this), responseText);
1305 + }
1306 +
1307 + if (sender === "bot" || sender === "agent") {
1308 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1309 + if (lastUserMessage.length) {
1310 + scrollElementToTop(lastUserMessage);
1311 + }
1312 + // Show notification if chat is hidden
1313 + if ($('#floating-chatbot').hasClass('hidden')) {
1314 + showNotification();
1315 + }
1316 + }
1317 + });
1318 + });
2225 1319 } else {
2226 - appendMessage(sender, responseText, responseHtml, images, false, botId);
2227 - // Re-enable chat input after response is displayed
2228 - enableChatInput(botId);
1320 + appendMessage(sender, responseText, responseHtml, images);
2229 1321 }
2230 1322 }
2231 1323
2232 1324
2233 - function appendThinkingMessage(botId) {
2234 - botId = botId || 'default';
1325 + function appendThinkingMessage() {
1326 + // Remove any existing thinking dots first
1327 + $('.thinking-dots').remove();
2235 1328
2236 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2237 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2238 - if (indicator && indicator.textContent === 'Live Agent') {
2239 - return;
2240 - }
2241 -
2242 - var $chatBox = getElement(botId, 'chat-box');
2243 -
2244 - // Remove any existing thinking dots in this bot's chat first
2245 - $chatBox.find('.thinking-dots').remove();
2246 -
2247 - // Check if we should skip inline colors (AI theme is active)
2248 - var skipColors = shouldSkipInlineColors(botId);
2249 -
2250 1329 // Retrieve the bot message font color and background color
2251 1330 var botMessageFontColor = mxchatChat.bot_message_font_color;
2252 1331 var botMessageBgColor = mxchatChat.bot_message_bg_color;
2253 1332
2254 - // Build thinking dots HTML - skip inline colors if AI theme is active
2255 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1333 +
2256 1334 var thinkingHtml = '<div class="thinking-dots-container">' +
2257 1335 '<div class="thinking-dots">' +
2258 - '<span class="dot"' + dotStyle + '></span>' +
2259 - '<span class="dot"' + dotStyle + '></span>' +
2260 - '<span class="dot"' + dotStyle + '></span>' +
1336 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1337 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1338 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
2261 1339 '</div>' +
2262 1340 '</div>';
2263 1341
2264 - // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2265 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
2266 - $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2267 - scrollToBottom(botId);
1342 + // Append the thinking dots to the chat container (or within the temporary message div)
1343 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1344 + scrollToBottom();
2268 1345 }
2269 -
2270 - function removeThinkingDots(botId) {
2271 - botId = botId || 'default';
2272 - var $chatBox = getElement(botId, 'chat-box');
2273 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2274 - $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2275 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1346 +
1347 + function removeThinkingDots() {
1348 + $('.thinking-dots').closest('.temporary-message').remove();
2276 1349 }
2277 1350
2278 1351 // ====================================
2279 1352 // TEXT FORMATTING & PROCESSING
@@ -2307,12 +1380,9 @@
2307 1380 processedText = formatTextStyling(processedText);
2308 1381
2309 1382 // Process code blocks BEFORE processing links
2310 1383 processedText = formatCodeBlocks(processedText);
2311 -
2312 - // Process markdown tables BEFORE converting newlines to paragraphs
2313 - processedText = formatMarkdownTables(processedText);
2314 -
1384 +
2315 1385 // NOW convert to paragraphs
2316 1386 processedText = convertNewlinesToBreaks(processedText);
2317 1387
2318 1388 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2325,63 +1395,37 @@
2325 1395 // Return as a proper link without the brackets
2326 1396 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2327 1397 });
2328 1398
2329 - // Process markdown links: [text](url) and [](url)
2330 - // Uses balanced parenthesis matching to handle URLs containing parens
2331 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2332 - processedText = (function(input) {
2333 - var result = '';
2334 - var i = 0;
2335 - while (i < input.length) {
2336 - // Look for [ at current position
2337 - if (input[i] === '[') {
2338 - // Find closing ]
2339 - var closeBracket = input.indexOf(']', i + 1);
2340 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2341 - result += input[i];
2342 - i++;
2343 - continue;
2344 - }
2345 - var linkText = input.substring(i + 1, closeBracket);
2346 - // Check if URL starts with http
2347 - var urlStart = closeBracket + 2;
2348 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2349 - result += input[i];
2350 - i++;
2351 - continue;
2352 - }
2353 - // Find balanced closing paren
2354 - var depth = 1;
2355 - var j = urlStart;
2356 - while (j < input.length && depth > 0) {
2357 - if (input[j] === '(') depth++;
2358 - else if (input[j] === ')') depth--;
2359 - if (depth > 0) j++;
2360 - }
2361 - if (depth !== 0) {
2362 - result += input[i];
2363 - i++;
2364 - continue;
2365 - }
2366 - var url = input.substring(urlStart, j);
2367 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2368 - var encodedUrl = safeEncodeUrl(cleanUrl);
2369 - if (!linkText || !linkText.trim()) {
2370 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2371 - } else {
2372 - var safeText = sanitizeUserInput(linkText);
2373 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2374 - }
2375 - i = j + 1; // Skip past the closing )
2376 - } else {
2377 - result += input[i];
2378 - i++;
2379 - }
1399 + // Process proper markdown links with text: [text](url)
1400 + // This MUST have non-empty text in the first brackets
1401 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1402 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1403 + // Make sure we have actual text (not just whitespace)
1404 + if (!text || !text.trim()) {
1405 + // If no text, treat the URL as the text
1406 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1407 + const safeUrl = safeEncodeUrl(cleanUrl);
1408 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2380 1409 }
2381 - return result;
2382 - })(processedText);
1410 +
1411 + // Clean the URL
1412 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1413 + const safeUrl = safeEncodeUrl(cleanUrl);
1414 + const safeText = sanitizeUserInput(text);
1415 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1416 + });
2383 1417
1418 + // Handle empty markdown links: [](url)
1419 + // This is a specific case where there's no text
1420 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1421 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1422 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1423 + const safeUrl = safeEncodeUrl(cleanUrl);
1424 + // Use the URL itself as the link text
1425 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1426 + });
1427 +
2384 1428 // Process phone numbers: [text](tel:number)
2385 1429 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2386 1430 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2387 1431 const safePhone = safeEncodeUrl(phone);
@@ -2533,78 +1577,9 @@
2533 1577 });
2534 1578
2535 1579 return text;
2536 1580 }
2537 -
2538 - function formatMarkdownTables(text) {
2539 - var lines = text.split('\n');
2540 - var result = [];
2541 - var i = 0;
2542 -
2543 - while (i < lines.length) {
2544 - // Check for a table: current line has pipes AND next line is a separator row
2545 - if (i + 1 < lines.length &&
2546 - lines[i].indexOf('|') !== -1 &&
2547 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2548 -
2549 - var tableLines = [];
2550 - var headerLine = lines[i];
2551 - var separatorLine = lines[i + 1];
2552 - tableLines.push(headerLine);
2553 - tableLines.push(separatorLine);
2554 -
2555 - // Collect remaining table rows
2556 - var j = i + 2;
2557 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2558 - tableLines.push(lines[j]);
2559 - j++;
2560 - }
2561 -
2562 - // Parse alignment from separator row
2563 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2564 - var alignments = sepCells.map(function(cell) {
2565 - var trimmed = cell.trim();
2566 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2567 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2568 - return 'left';
2569 - });
2570 -
2571 - // Build HTML table
2572 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2573 -
2574 - // Header row
2575 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2576 - html += '<thead><tr>';
2577 - headerCells.forEach(function(cell, idx) {
2578 - var align = alignments[idx] || 'left';
2579 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2580 - });
2581 - html += '</tr></thead>';
2582 -
2583 - // Body rows
2584 - html += '<tbody>';
2585 - for (var r = 2; r < tableLines.length; r++) {
2586 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2587 - html += '<tr>';
2588 - rowCells.forEach(function(cell, idx) {
2589 - var align = alignments[idx] || 'left';
2590 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2591 - });
2592 - html += '</tr>';
2593 - }
2594 - html += '</tbody></table></div>';
2595 -
2596 - result.push(html);
2597 - i = j;
2598 - } else {
2599 - result.push(lines[i]);
2600 - i++;
2601 - }
2602 - }
2603 -
2604 - return result.join('\n');
2605 - }
2606 -
1581 +
2607 1582 function sanitizeUserInput(text) {
2608 1583 const div = document.createElement('div');
2609 1584 div.textContent = text;
2610 1585 return div.innerHTML;
@@ -2634,21 +1609,10 @@
2634 1609 // ====================================
2635 1610 // UI & SCROLLING CONTROLS
2636 1611 // ====================================
2637 1612
2638 - function scrollToBottom(botIdOrInstant, instant) {
2639 - // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
2640 - var botId = 'default';
2641 - if (typeof botIdOrInstant === 'string') {
2642 - botId = botIdOrInstant;
2643 - instant = instant || false;
2644 - } else if (typeof botIdOrInstant === 'boolean') {
2645 - instant = botIdOrInstant;
2646 - } else {
2647 - instant = false;
2648 - }
2649 -
2650 - var chatBox = getElement(botId, 'chat-box');
1613 + function scrollToBottom(instant = false) {
1614 + var chatBox = $('#chat-box');
2651 1615 if (instant) {
2652 1616 // Instantly set the scroll position to the bottom
2653 1617 chatBox.scrollTop(chatBox.prop("scrollHeight"));
2654 1618 } else {
@@ -2657,15 +1621,15 @@
2657 1621 const scrollHeight = chatBox.prop("scrollHeight");
2658 1622 const initialScroll = chatBox.scrollTop();
2659 1623 const distance = scrollHeight - initialScroll;
2660 1624 const duration = 500; // Duration in ms
2661 -
1625 +
2662 1626 function smoothScroll(timestamp) {
2663 1627 if (!start) start = timestamp;
2664 1628 const progress = timestamp - start;
2665 1629 const currentScroll = initialScroll + (distance * (progress / duration));
2666 1630 chatBox.scrollTop(currentScroll);
2667 -
1631 +
2668 1632 if (progress < duration) {
2669 1633 requestAnimationFrame(smoothScroll);
2670 1634 } else {
2671 1635 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -2670,37 +1634,33 @@
2670 1634 } else {
2671 1635 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2672 1636 }
2673 1637 }
2674 -
1638 +
2675 1639 requestAnimationFrame(smoothScroll);
2676 1640 }
2677 1641 }
2678 -
2679 - function scrollElementToTop(element, botId, topOffset) {
2680 - botId = botId || 'default';
2681 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2682 - var chatBox = getElement(botId, 'chat-box');
1642 +
1643 + function scrollElementToTop(element) {
1644 + var chatBox = $('#chat-box');
2683 1645 var elementTop = element.position().top + chatBox.scrollTop();
2684 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1646 + chatBox.animate({ scrollTop: elementTop }, 500);
2685 1647 }
2686 -
2687 - function showChatWidget(botId) {
2688 - botId = botId || 'default';
2689 - var $button = getElement(botId, 'floating-chatbot-button');
1648 +
1649 + function showChatWidget() {
2690 1650 // First ensure display is set
2691 - $button.css('display', 'flex');
1651 + $('#floating-chatbot-button').css('display', 'flex');
2692 1652 // Then handle the fade
2693 - $button.fadeTo(500, 1);
1653 + $('#floating-chatbot-button').fadeTo(500, 1);
2694 1654 // Force visibility
2695 - $button.removeClass('hidden');
1655 + $('#floating-chatbot-button').removeClass('hidden');
1656 + //console.log('Showing widget');
2696 1657 }
2697 -
2698 - function hideChatWidget(botId) {
2699 - botId = botId || 'default';
2700 - var $button = getElement(botId, 'floating-chatbot-button');
2701 - $button.css('display', 'none');
2702 - $button.addClass('hidden');
1658 +
1659 + function hideChatWidget() {
1660 + $('#floating-chatbot-button').css('display', 'none');
1661 + $('#floating-chatbot-button').addClass('hidden');
1662 + //console.log('Hiding widget');
2703 1663 }
2704 1664
2705 1665 function disableScroll() {
2706 1666 if (isMobile()) {
@@ -2729,15 +1689,18 @@
2729 1689 // NOTIFICATION SYSTEM
2730 1690 // ====================================
2731 1691
2732 1692 function createNotificationBadge() {
1693 + //console.log("Creating notification badge...");
2733 1694 const chatButton = document.getElementById('floating-chatbot-button');
2734 -
1695 + //console.log("Chat button found:", !!chatButton);
1696 +
2735 1697 if (!chatButton) return;
2736 -
1698 +
2737 1699 // Remove any existing badge first
2738 1700 const existingBadge = chatButton.querySelector('.chat-notification-badge');
2739 1701 if (existingBadge) {
1702 + //console.log("Removing existing badge");
2740 1703 existingBadge.remove();
2741 1704 }
2742 1705
2743 1706 notificationBadge = document.createElement('div');
@@ -2759,43 +1722,34 @@
2759 1722 chatButton.appendChild(notificationBadge);
2760 1723
2761 1724 }
2762 1725
2763 - function showNotification(botId) {
2764 - botId = botId || 'default';
2765 - const badge = getElementDOM(botId, 'chat-notification-badge');
2766 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2767 - if (badge && $floatingChatbot.hasClass('hidden')) {
1726 + function showNotification() {
1727 + const badge = document.getElementById('chat-notification-badge');
1728 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2768 1729 badge.style.display = 'block';
2769 1730 badge.textContent = '1';
2770 1731 }
2771 1732 }
2772 -
2773 - function hideNotification(botId) {
2774 - botId = botId || 'default';
2775 - const badge = getElementDOM(botId, 'chat-notification-badge');
1733 +
1734 + function hideNotification() {
1735 + const badge = document.getElementById('chat-notification-badge');
2776 1736 if (badge) {
2777 1737 badge.style.display = 'none';
2778 1738 }
2779 1739 }
2780 -
2781 - function startNotificationChecking(botId) {
2782 - botId = botId || 'default';
1740 +
1741 + function startNotificationChecking() {
2783 1742 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2784 1743 if (!chatPersistenceEnabled) return;
2785 -
2786 - createNotificationBadge(botId);
2787 - var instance = MxChatInstances.get(botId);
2788 - instance.notificationCheckInterval = setInterval(function() {
2789 - checkForNewMessages(botId);
2790 - }, 30000); // Check every 30 seconds
1744 +
1745 + createNotificationBadge();
1746 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2791 1747 }
2792 -
2793 - function stopNotificationChecking(botId) {
2794 - botId = botId || 'default';
2795 - var instance = MxChatInstances.get(botId);
2796 - if (instance.notificationCheckInterval) {
2797 - clearInterval(instance.notificationCheckInterval);
1748 +
1749 + function stopNotificationChecking() {
1750 + if (notificationCheckInterval) {
1751 + clearInterval(notificationCheckInterval);
2798 1752 }
2799 1753 }
2800 1754
2801 1755 function checkForNewMessages() {
@@ -2821,35 +1775,63 @@
2821 1775 });
2822 1776 }
2823 1777
2824 1778
2825 -// ====================================
2826 -// LIVE AGENT FUNCTIONALITY
2827 -// ====================================
2828 -
2829 -function startPolling(botId) {
2830 - botId = botId || 'default';
2831 - var instance = MxChatInstances.get(botId);
2832 - // Clear any existing interval first
2833 - stopPolling(botId);
2834 - instance.pollingInterval = setInterval(function() {
2835 - checkForAgentMessages(botId);
2836 - }, 5000);
1779 + // ====================================
1780 + // LIVE AGENT FUNCTIONALITY
1781 + // ====================================
1782 +
1783 +function updateChatModeIndicator(mode) {
1784 + //console.log("updateChatModeIndicator called with mode:", mode);
1785 + const indicator = document.getElementById('chat-mode-indicator');
1786 + if (indicator) {
1787 + const oldText = indicator.textContent;
1788 +
1789 + if (mode === 'agent') {
1790 + indicator.textContent = 'Live Agent';
1791 + startPolling();
1792 + //console.log("Set indicator to Live Agent");
1793 + } else {
1794 + // Everything else is AI mode
1795 + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1796 + indicator.textContent = customAiText;
1797 + stopPolling();
1798 + //console.log("Set indicator to AI Agent:", customAiText);
1799 + }
1800 +
1801 + // Force immediate DOM update
1802 + if (oldText !== indicator.textContent) {
1803 + // Force a reflow to ensure the change is visible immediately
1804 + indicator.offsetHeight;
1805 +
1806 + // Double-check after a brief moment
1807 + setTimeout(() => {
1808 + //console.log("Final indicator text:", indicator.textContent);
1809 + }, 50);
1810 + }
1811 + } else {
1812 + //console.error("Chat mode indicator element not found!");
1813 + }
2837 1814 }
2838 1815
2839 -function stopPolling(botId) {
2840 - botId = botId || 'default';
2841 - var instance = MxChatInstances.get(botId);
2842 - if (instance.pollingInterval) {
2843 - clearInterval(instance.pollingInterval);
2844 - instance.pollingInterval = null;
1816 + function startPolling() {
1817 + // Clear any existing interval first
1818 + stopPolling();
1819 + // Start new polling interval
1820 + pollingInterval = setInterval(checkForAgentMessages, 5000);
1821 + //console.log("Started agent message polling");
2845 1822 }
2846 -}
2847 -
2848 -function checkForAgentMessages(botId) {
2849 - botId = botId || 'default';
2850 - var instance = MxChatInstances.get(botId);
2851 - const sessionId = getChatSession(botId);
1823 +
1824 + function stopPolling() {
1825 + if (pollingInterval) {
1826 + clearInterval(pollingInterval);
1827 + pollingInterval = null;
1828 + //console.log("Stopped agent message polling");
1829 + }
1830 + }
1831 +
1832 +function checkForAgentMessages() {
1833 + const sessionId = getChatSession();
2852 1834 $.ajax({
2853 1835 url: mxchatChat.ajax_url,
2854 1836 type: 'POST',
2855 1837 dataType: 'json',
@@ -2855,44 +1837,35 @@
2855 1837 dataType: 'json',
2856 1838 data: {
2857 1839 action: 'mxchat_fetch_new_messages',
2858 1840 session_id: sessionId,
2859 - last_seen_id: instance.lastSeenMessageId,
2860 - persistence_enabled: 'true',
1841 + last_seen_id: lastSeenMessageId,
1842 + persistence_enabled: 'true', // Add this too
2861 1843 nonce: mxchatChat.nonce
2862 1844 },
2863 1845 success: function (response) {
2864 1846 if (response.success && response.data?.new_messages) {
2865 1847 let hasNewMessage = false;
2866 -
1848 +
2867 1849 response.data.new_messages.forEach(function (message) {
2868 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1850 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2869 1851 hasNewMessage = true;
2870 - appendMessage("agent", message.content, '', [], false, botId);
2871 - instance.lastSeenMessageId = message.id;
2872 - instance.processedMessageIds.add(message.id);
1852 + // CHANGE THIS LINE:
1853 + appendMessage("agent", message.content); // Instead of replaceLastMessage
1854 + lastSeenMessageId = message.id;
1855 + processedMessageIds.add(message.id);
2873 1856 }
2874 1857 });
2875 1858
2876 - if (hasNewMessage) {
2877 - enableChatInput(botId);
1859 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1860 + showNotification();
2878 1861 }
2879 -
2880 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2881 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2882 - showNotification(botId);
2883 - }
2884 -
2885 - scrollToBottom(botId, true);
1862 +
1863 + scrollToBottom(true);
2886 1864 }
2887 -
2888 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2889 - if (response.success && response.data?.chat_mode) {
2890 - updateChatModeIndicator(response.data.chat_mode, botId);
2891 - }
2892 1865 },
2893 1866 error: function (xhr, status, error) {
2894 - // Polling error - silently continue
1867 + //console.error("Polling error:", xhr, status, error);
2895 1868 }
2896 1869 });
2897 1870 }
2898 1871
@@ -2898,30 +1871,19 @@
2898 1871
2899 1872 // ====================================
2900 1873 // CHAT HISTORY & PERSISTENCE
2901 1874 // ====================================
2902 -
2903 -function loadChatHistory(botId, onComplete) {
2904 - botId = botId || 'default';
2905 - var instance = MxChatInstances.get(botId);
2906 -
1875 +// Update the loadChatHistory function:
1876 +function loadChatHistory() {
2907 1877 // Prevent duplicate loading
2908 - if (instance.chatHistoryLoaded) {
2909 - if (onComplete) onComplete();
1878 + if (chatHistoryLoaded) {
1879 + // console.log('Chat history already loaded, skipping...');
2910 1880 return;
2911 1881 }
2912 -
2913 - // Use getChatSession which returns null if no session exists (does NOT create one)
2914 - var sessionId = getChatSession(botId);
1882 +
1883 + var sessionId = getChatSession();
2915 1884 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2916 1885
2917 - // No session yet — nothing to load. History will load after first message via ensureSession.
2918 - if (!sessionId) {
2919 - instance.chatHistoryLoaded = true;
2920 - if (onComplete) onComplete();
2921 - return;
2922 - }
2923 -
2924 1886 if (chatPersistenceEnabled && sessionId) {
2925 1887 $.ajax({
2926 1888 url: mxchatChat.ajax_url,
2927 1889 type: 'POST',
@@ -2932,12 +1894,11 @@
2932 1894 },
2933 1895 success: function(response) {
2934 1896 // Handle session reset (IP changed while user was away)
2935 1897 if (response.success === false && response.data && response.data.action === 'reset_session') {
2936 - // Silent reset — new session but don't clear UI
2937 - MxChatInstances.silentResetSession(botId);
2938 - instance.chatHistoryLoaded = true; // Prevent retry loop
2939 - if (onComplete) onComplete();
1898 + // Silently reset session - user will start fresh
1899 + resetChatSession();
1900 + chatHistoryLoaded = true; // Prevent retry loop
2940 1901 return;
2941 1902 }
2942 1903
2943 1904 // Check if the response indicates success
@@ -2943,15 +1904,15 @@
2943 1904 // Check if the response indicates success
2944 1905 if (response.success) {
2945 1906 // Handle case where conversation data exists and is an array
2946 1907 if (response.data && Array.isArray(response.data.conversation)) {
2947 - var $chatBox = getElement(botId, 'chat-box');
1908 + var $chatBox = $('#chat-box');
2948 1909 var $fragment = $(document.createDocumentFragment());
2949 - let highestMessageId = instance.lastSeenMessageId;
1910 + let highestMessageId = lastSeenMessageId;
2950 1911
2951 1912 // Update chat mode if provided
2952 1913 if (response.data.chat_mode) {
2953 - updateChatModeIndicator(response.data.chat_mode, botId);
1914 + updateChatModeIndicator(response.data.chat_mode);
2954 1915 }
2955 1916
2956 1917 // Only process if there are actual messages
2957 1918 if (response.data.conversation.length > 0) {
@@ -2956,9 +1917,9 @@
2956 1917 // Only process if there are actual messages
2957 1918 if (response.data.conversation.length > 0) {
2958 1919 // IMPORTANT: Clear existing messages before loading history
2959 1920 $chatBox.empty();
2960 -
1921 +
2961 1922 $.each(response.data.conversation, function(index, message) {
2962 1923 // Skip agent messages if persistence is off
2963 1924 if (!chatPersistenceEnabled && message.role === 'agent') {
2964 1925 return;
@@ -2993,19 +1954,9 @@
2993 1954 var content = message.content;
2994 1955 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2995 1956 content = decodeHTMLEntities(content);
2996 1957
2997 - // Skip linkify for messages containing structured HTML
2998 - // (forms, product cards, galleries, etc.) to avoid
2999 - // markdown formatting corrupting HTML attributes
3000 - // (e.g. underscores in name="field_name" becoming <em> tags)
3001 - if (content.includes("mxchat-product-card") ||
3002 - content.includes("mxchat-image-gallery") ||
3003 - content.includes("mxchat-featured-products") ||
3004 - content.includes("<form") ||
3005 - content.includes("<input") ||
3006 - content.includes("<select") ||
3007 - content.includes("<textarea")) {
1958 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
3008 1959 messageElement.html(content);
3009 1960 } else {
3010 1961 var formattedContent = linkify(content);
3011 1962 messageElement.html(formattedContent);
@@ -3015,47 +1966,42 @@
3015 1966
3016 1967 // Track message IDs
3017 1968 if (message.id) {
3018 1969 highestMessageId = Math.max(highestMessageId, message.id);
3019 - instance.processedMessageIds.add(message.id);
1970 + processedMessageIds.add(message.id);
3020 1971 }
3021 1972 });
3022 1973
3023 1974 // Only append messages and scroll if we have content
3024 1975 $chatBox.append($fragment);
3025 - scrollToBottom(botId, true);
1976 + scrollToBottom(true);
3026 1977
3027 1978 // Collapse quick questions if we have conversation history
3028 - // BUT skip auto-collapse for embedded bots (they should stay expanded)
3029 - if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
3030 - collapseQuickQuestions(botId);
1979 + if (hasQuickQuestions()) {
1980 + collapseQuickQuestions();
3031 1981 }
3032 1982
3033 1983 // Update lastSeenMessageId after history loads
3034 - instance.lastSeenMessageId = highestMessageId;
1984 + lastSeenMessageId = highestMessageId;
3035 1985
3036 1986 // Only update chat mode if persistence is enabled and we have messages
3037 1987 if (chatPersistenceEnabled) {
3038 1988 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
3039 1989 if (lastMessage.role === 'agent') {
3040 - updateChatModeIndicator('agent', botId);
1990 + updateChatModeIndicator('agent');
3041 1991 }
3042 1992 }
3043 -
1993 +
3044 1994 // Mark as loaded ONLY after successful load
3045 - instance.chatHistoryLoaded = true;
1995 + chatHistoryLoaded = true;
3046 1996 }
3047 1997 }
3048 1998 }
3049 - if (onComplete) onComplete();
3050 1999 },
3051 2000 error: function(xhr, status, error) {
3052 - // Error loading chat history - silently continue
3053 - if (onComplete) onComplete();
2001 + //console.error("AJAX error loading chat history:", status, error);
3054 2002 }
3055 2003 });
3056 - } else {
3057 - if (onComplete) onComplete();
3058 2004 }
3059 2005 }
3060 2006
3061 2007
@@ -3069,42 +2015,40 @@
3069 2015 element.addEventListener(eventType, handler);
3070 2016 }
3071 2017 }
3072 2018
3073 - function showActivePdf(filename, botId) {
3074 - botId = botId || 'default';
3075 - const container = getElementDOM(botId, 'active-pdf-container');
3076 - const nameElement = getElementDOM(botId, 'active-pdf-name');
3077 -
2019 + function showActivePdf(filename) {
2020 + const container = document.getElementById('active-pdf-container');
2021 + const nameElement = document.getElementById('active-pdf-name');
2022 +
3078 2023 if (!container || !nameElement) {
2024 + //console.error('PDF container elements not found');
3079 2025 return;
3080 2026 }
3081 -
2027 +
3082 2028 nameElement.textContent = filename;
3083 2029 container.style.display = 'flex';
3084 2030 }
3085 -
3086 - function showActiveWord(filename, botId) {
3087 - botId = botId || 'default';
3088 - const container = getElementDOM(botId, 'active-word-container');
3089 - const nameElement = getElementDOM(botId, 'active-word-name');
3090 -
2031 +
2032 + function showActiveWord(filename) {
2033 + const container = document.getElementById('active-word-container');
2034 + const nameElement = document.getElementById('active-word-name');
2035 +
3091 2036 if (!container || !nameElement) {
2037 + //console.error('Word document container elements not found');
3092 2038 return;
3093 2039 }
3094 -
2040 +
3095 2041 nameElement.textContent = filename;
3096 2042 container.style.display = 'flex';
3097 2043 }
3098 -
3099 - function removeActivePdf(botId) {
3100 - botId = botId || 'default';
3101 - var instance = MxChatInstances.get(botId);
3102 - const container = getElementDOM(botId, 'active-pdf-container');
3103 - const nameElement = getElementDOM(botId, 'active-pdf-name');
3104 -
3105 - if (!container || !nameElement || !instance.activePdfFile) return;
3106 -
2044 +
2045 + function removeActivePdf() {
2046 + const container = document.getElementById('active-pdf-container');
2047 + const nameElement = document.getElementById('active-pdf-name');
2048 +
2049 + if (!container || !nameElement || !activePdfFile) return;
2050 +
3107 2051 fetch(mxchatChat.ajax_url, {
3108 2052 method: 'POST',
3109 2053 headers: {
3110 2054 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3110,9 +2054,9 @@
3110 2054 'Content-Type': 'application/x-www-form-urlencoded',
3111 2055 },
3112 2056 body: new URLSearchParams({
3113 2057 'action': 'mxchat_remove_pdf',
3114 - 'session_id': getChatSession(botId),
2058 + 'session_id': sessionId,
3115 2059 'nonce': mxchatChat.nonce
3116 2060 })
3117 2061 })
3118 2062 .then(response => response.json())
@@ -3119,25 +2063,23 @@
3119 2063 .then(data => {
3120 2064 if (data.success) {
3121 2065 container.style.display = 'none';
3122 2066 nameElement.textContent = '';
3123 - instance.activePdfFile = null;
3124 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2067 + activePdfFile = null;
2068 + appendMessage('bot', 'PDF removed.');
3125 2069 }
3126 2070 })
3127 2071 .catch(error => {
3128 - // Error removing PDF - silently continue
2072 + //console.error('Error removing PDF:', error);
3129 2073 });
3130 2074 }
3131 -
3132 - function removeActiveWord(botId) {
3133 - botId = botId || 'default';
3134 - var instance = MxChatInstances.get(botId);
3135 - const container = getElementDOM(botId, 'active-word-container');
3136 - const nameElement = getElementDOM(botId, 'active-word-name');
3137 -
3138 - if (!container || !nameElement || !instance.activeWordFile) return;
3139 -
2075 +
2076 + function removeActiveWord() {
2077 + const container = document.getElementById('active-word-container');
2078 + const nameElement = document.getElementById('active-word-name');
2079 +
2080 + if (!container || !nameElement || !activeWordFile) return;
2081 +
3140 2082 fetch(mxchatChat.ajax_url, {
3141 2083 method: 'POST',
3142 2084 headers: {
3143 2085 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3143,9 +2085,9 @@
3143 2085 'Content-Type': 'application/x-www-form-urlencoded',
3144 2086 },
3145 2087 body: new URLSearchParams({
3146 2088 'action': 'mxchat_remove_word',
3147 - 'session_id': getChatSession(botId),
2089 + 'session_id': sessionId,
3148 2090 'nonce': mxchatChat.nonce
3149 2091 })
3150 2092 })
3151 2093 .then(response => response.json())
@@ -3152,58 +2094,61 @@
3152 2094 .then(data => {
3153 2095 if (data.success) {
3154 2096 container.style.display = 'none';
3155 2097 nameElement.textContent = '';
3156 - instance.activeWordFile = null;
3157 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2098 + activeWordFile = null;
2099 + appendMessage('bot', 'Word document removed.');
3158 2100 }
3159 2101 })
3160 2102 .catch(error => {
3161 - // Error removing Word document - silently continue
2103 + //console.error('Error removing Word document:', error);
3162 2104 });
3163 2105 }
3164 -
2106 +
3165 2107 // ====================================
3166 2108 // CONSENT & COMPLIANCE (GDPR)
3167 2109 // ====================================
3168 -
3169 - function initializeChatVisibility(botId) {
3170 - botId = botId || 'default';
3171 - const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
3172 - mxchatChat.complianz_toggle === '1' ||
2110 +
2111 + function initializeChatVisibility() {
2112 + //console.log('Initializing chat visibility');
2113 + const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2114 + mxchatChat.complianz_toggle === '1' ||
3173 2115 mxchatChat.complianz_toggle === 1;
3174 -
2116 +
3175 2117 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
3176 2118 // Initial check
3177 - checkConsentAndShowChat(botId);
3178 -
2119 + checkConsentAndShowChat();
2120 +
3179 2121 // Listen for consent changes
3180 2122 $(document).on('cmplz_status_change', function(event) {
3181 - checkConsentAndShowChat(botId);
2123 + //console.log('Status change detected');
2124 + checkConsentAndShowChat();
3182 2125 });
3183 2126 } else {
3184 2127 // If Complianz is not enabled, always show
3185 - getElement(botId, 'floating-chatbot-button')
2128 + $('#floating-chatbot-button')
3186 2129 .css('display', 'flex')
3187 2130 .removeClass('hidden no-consent')
3188 2131 .fadeTo(500, 1);
3189 -
2132 +
3190 2133 // Also check pre-chat message when Complianz is not enabled
3191 - checkPreChatDismissal(botId);
2134 + checkPreChatDismissal();
3192 2135 }
3193 2136 }
3194 2137
3195 -
3196 - function checkConsentAndShowChat(botId) {
3197 - botId = botId || 'default';
2138 +
2139 + function checkConsentAndShowChat() {
3198 2140 var consentStatus = cmplz_has_consent('marketing');
3199 2141 var consentType = complianz.consenttype;
3200 -
3201 - let $widget = getElement(botId, 'floating-chatbot-button');
3202 - let $chatbot = getElement(botId, 'floating-chatbot');
3203 - let $preChat = getElement(botId, 'pre-chat-message');
3204 -
2142 +
2143 + //console.log('Checking consent:', {status: consentStatus,type: consentType});
2144 +
2145 + let $widget = $('#floating-chatbot-button');
2146 + let $chatbot = $('#floating-chatbot');
2147 + let $preChat = $('#pre-chat-message');
2148 +
3205 2149 if (consentStatus === true) {
2150 + //console.log('Consent granted - showing widget');
3206 2151 $widget
3207 2152 .removeClass('no-consent')
3208 2153 .css('display', 'flex')
3209 2154 .removeClass('hidden')
@@ -3208,12 +2153,13 @@
3208 2153 .css('display', 'flex')
3209 2154 .removeClass('hidden')
3210 2155 .fadeTo(500, 1);
3211 2156 $chatbot.removeClass('no-consent');
3212 -
2157 +
3213 2158 // Show pre-chat message if not dismissed
3214 - checkPreChatDismissal(botId);
2159 + checkPreChatDismissal();
3215 2160 } else {
2161 + //console.log('No consent - hiding widget');
3216 2162 $widget
3217 2163 .addClass('no-consent')
3218 2164 .fadeTo(500, 0, function() {
3219 2165 $(this)
@@ -3220,9 +2166,9 @@
3220 2166 .css('display', 'none')
3221 2167 .addClass('hidden');
3222 2168 });
3223 2169 $chatbot.addClass('no-consent');
3224 -
2170 +
3225 2171 // Hide pre-chat message when no consent
3226 2172 $preChat.hide();
3227 2173 }
3228 2174 }
@@ -3230,38 +2176,46 @@
3230 2176
3231 2177 // ====================================
3232 2178 // PRE-CHAT MESSAGE HANDLING
3233 2179 // ====================================
3234 -
3235 - function checkPreChatDismissal(botId) {
3236 - botId = botId || 'default';
3237 - try {
3238 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3239 - if (dismissedAt) {
3240 - // Re-show after 24 hours
3241 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3242 - if (elapsed < 86400000) {
3243 - getElement(botId, 'pre-chat-message').hide();
3244 - return;
2180 +
2181 + function checkPreChatDismissal() {
2182 + $.ajax({
2183 + url: mxchatChat.ajax_url,
2184 + type: 'POST',
2185 + data: {
2186 + action: 'mxchat_check_pre_chat_message_status',
2187 + _ajax_nonce: mxchatChat.nonce
2188 + },
2189 + success: function(response) {
2190 + if (response.success && !response.data.dismissed) {
2191 + $('#pre-chat-message').fadeIn(250);
2192 + } else {
2193 + $('#pre-chat-message').hide();
3245 2194 }
3246 - // Expired — clear and show again
3247 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2195 + },
2196 + error: function() {
2197 + //console.error('Failed to check pre-chat message dismissal status.');
3248 2198 }
3249 - getElement(botId, 'pre-chat-message').fadeIn(250);
3250 - } catch (e) {
3251 - // localStorage unavailable — show the message
3252 - getElement(botId, 'pre-chat-message').fadeIn(250);
3253 - }
2199 + });
3254 2200 }
3255 -
3256 - function handlePreChatDismissal(botId) {
3257 - botId = botId || 'default';
3258 - getElement(botId, 'pre-chat-message').fadeOut(200);
3259 - try {
3260 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3261 - } catch (e) {
3262 - // localStorage unavailable — dismissal won't persist
3263 - }
2201 +
2202 + function handlePreChatDismissal() {
2203 + $('#pre-chat-message').fadeOut(200);
2204 + $.ajax({
2205 + url: mxchatChat.ajax_url,
2206 + type: 'POST',
2207 + data: {
2208 + action: 'mxchat_dismiss_pre_chat_message',
2209 + _ajax_nonce: mxchatChat.nonce
2210 + },
2211 + success: function() {
2212 + $('#pre-chat-message').hide();
2213 + },
2214 + error: function() {
2215 + //console.error('Failed to dismiss pre-chat message.');
2216 + }
2217 + });
3264 2218 }
3265 2219
3266 2220
3267 2221 // ====================================
@@ -3287,307 +2241,231 @@
3287 2241 // ====================================
3288 2242
3289 2243 $(document).on('click', '.mxchat-popular-question', function () {
3290 2244 var question = $(this).text();
3291 - var botId = getBotIdFromElement(this);
3292 -
2245 +
3293 2246 // Append the question as if the user typed it
3294 - appendMessage("user", question, '', [], false, botId);
3295 -
2247 + appendMessage("user", question);
2248 +
3296 2249 // Only collapse if there are questions
3297 - if (hasQuickQuestions(botId)) {
3298 - collapseQuickQuestions(botId);
2250 + if (hasQuickQuestions()) {
2251 + collapseQuickQuestions();
3299 2252 }
3300 -
2253 +
3301 2254 // Send the question to the server
3302 - sendMessageToChatbot(question, botId);
2255 + sendMessageToChatbot(question);
3303 2256 });
3304 2257
3305 2258 $(document).on('click', '.questions-toggle-btn', function(e) {
3306 2259 e.preventDefault();
3307 2260 e.stopPropagation();
3308 - var botId = getBotIdFromElement(this);
3309 - expandQuickQuestions(botId);
2261 + expandQuickQuestions();
3310 2262 });
3311 2263
3312 2264 $(document).on('click', '.questions-collapse-btn', function(e) {
3313 2265 e.preventDefault();
3314 2266 e.stopPropagation();
3315 - var botId = getBotIdFromElement(this);
3316 - collapseQuickQuestions(botId);
2267 + collapseQuickQuestions();
3317 2268 });
3318 2269
3319 - // Chatbot visibility toggle handlers - use class selector for multi-instance support
3320 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3321 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3322 - if (e.type === 'keydown') {
3323 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3324 - e.preventDefault();
3325 - }
3326 - var botId = getBotIdFromElement(this);
3327 - var $chatbot = getElement(botId, 'floating-chatbot');
3328 - var $badge = getElement(botId, 'chat-notification-badge');
3329 - var $preChat = getElement(botId, 'pre-chat-message');
3330 -
3331 - if ($chatbot.hasClass('hidden')) {
3332 - $chatbot.removeClass('hidden').addClass('visible')
3333 - .attr('aria-modal', 'true').attr('role', 'dialog');
3334 - $(this).addClass('hidden').attr('aria-expanded', 'true');
3335 - $badge.hide(); // Hide notification when opening chat
2270 + // Chatbot visibility toggle handlers
2271 + $(document).on('click', '#floating-chatbot-button', function() {
2272 + var chatbot = $('#floating-chatbot');
2273 + if (chatbot.hasClass('hidden')) {
2274 + chatbot.removeClass('hidden').addClass('visible');
2275 + $(this).addClass('hidden');
2276 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
3336 2277 disableScroll();
3337 - $preChat.fadeOut(250);
3338 -
3339 - // First open per page load: re-fetch behavior settings in case
3340 - // this page's inline values came from a stale full-page cache
3341 - // (plan-32db95). Idempotent — later opens are a no-op.
3342 - mxchatRefreshDynamicSettings();
3343 -
3344 - // Load chat history for returning visitors (persistence)
3345 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3346 - if (chatPersistenceEnabled) {
3347 - MxChatInstances.ensureSession(botId);
3348 - }
3349 -
3350 - // Deferred email check — only on first widget open
3351 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3352 - var instance = MxChatInstances.get(botId);
3353 - if (emailBlocker && !instance.emailCheckDone) {
3354 - instance.emailCheckDone = true;
3355 - resolveEmailState(botId);
3356 - } else if (!emailBlocker) {
3357 - // No email collection — still route through showChatContainerForBot
3358 - // so the loader is shown while chat history loads
3359 - showChatContainerForBot(botId);
3360 - }
3361 -
3362 - // Move keyboard focus into the message input after the open transition.
3363 - setTimeout(function() {
3364 - var chatInput = getElementDOM(botId, 'chat-input');
3365 - if (chatInput && !chatInput.disabled) {
3366 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3367 - }
3368 - }, 300);
2278 + $('#pre-chat-message').fadeOut(250);
3369 2279 } else {
3370 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3371 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2280 + chatbot.removeClass('visible').addClass('hidden');
2281 + $(this).removeClass('hidden');
3372 2282 enableScroll();
3373 - checkPreChatDismissal(botId);
2283 + checkPreChatDismissal();
3374 2284 }
3375 2285 });
3376 -
3377 - // Allow clicking anywhere on the title bar to close the chatbot.
3378 - // Returns keyboard focus to the launcher so keyboard users don't get
3379 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3380 - // heuristic-based so mouse-triggered close won't show a focus ring.
3381 - $(document).on('click', '.chatbot-top-bar', function() {
3382 - var botId = getBotIdFromElement(this);
3383 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3384 - var $launcher = getElement(botId, 'floating-chatbot-button');
3385 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2286 +
2287 + $(document).on('click', '#exit-chat-button', function() {
2288 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
2289 + $('#floating-chatbot-button').removeClass('hidden');
3386 2290 enableScroll();
3387 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3388 2291 });
3389 -
3390 - // Global Escape-key handler — closes any visible chat widget and
3391 - // returns focus to its launcher. Standard modal-dismissal pattern;
3392 - // pairs with aria-modal="true" set on the widget when it opens.
3393 - $(document).on('keydown', function(e) {
3394 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3395 - var $visible = $('.floating-chatbot.visible');
3396 - if (!$visible.length) return;
3397 - e.preventDefault();
3398 - $visible.each(function() {
3399 - var botId = getBotIdFromElement(this);
3400 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3401 - var $launcher = getElement(botId, 'floating-chatbot-button');
3402 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3403 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3404 - });
3405 - enableScroll();
3406 - });
3407 -
2292 +
3408 2293 $(document).on('click', '.close-pre-chat-message', function(e) {
3409 2294 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3410 - var botId = getBotIdFromElement(this);
3411 - handlePreChatDismissal(botId);
2295 + $('#pre-chat-message').fadeOut(200, function() {
2296 + $(this).remove();
2297 + });
3412 2298 });
2299 +
3413 2300
3414 -
3415 - // PDF upload button handlers - use class selector
3416 - $(document).on('click', '.pdf-upload-btn', function() {
3417 - var botId = getBotIdFromElement(this);
3418 - var pdfInput = getElementDOM(botId, 'pdf-upload');
3419 - if (pdfInput) pdfInput.click();
3420 - });
3421 -
3422 - // Word upload button handlers - use class selector
3423 - $(document).on('click', '.word-upload-btn', function() {
3424 - var botId = getBotIdFromElement(this);
3425 - var wordInput = getElementDOM(botId, 'word-upload');
3426 - if (wordInput) wordInput.click();
3427 - });
2301 + // PDF upload button handlers
2302 + if (document.getElementById('pdf-upload-btn')) {
2303 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2304 + document.getElementById('pdf-upload').click();
2305 + });
2306 + }
3428 2307
3429 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3430 - $(document).on('change', '.pdf-upload', async function(e) {
3431 - var botId = getBotIdFromElement(this);
3432 - var instance = MxChatInstances.get(botId);
3433 - const file = this.files[0];
3434 - const sessionId = MxChatInstances.ensureSession(botId);
3435 -
2308 + // Word upload button handlers
2309 + if (document.getElementById('word-upload-btn')) {
2310 + document.getElementById('word-upload-btn').addEventListener('click', function() {
2311 + document.getElementById('word-upload').click();
2312 + });
2313 + }
2314 +
2315 + // PDF file input change handler
2316 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2317 + const file = e.target.files[0];
2318 +
3436 2319 if (!file || file.type !== 'application/pdf') {
3437 2320 alert('Please select a valid PDF file.');
3438 2321 return;
3439 2322 }
3440 -
2323 +
3441 2324 if (!sessionId) {
2325 + //console.error('No session ID found');
3442 2326 alert('Error: No session ID found');
3443 2327 return;
3444 2328 }
3445 -
2329 +
3446 2330 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2331 + //console.error('mxchatChat not properly configured:', mxchatChat);
3447 2332 alert('Error: Ajax configuration missing');
3448 2333 return;
3449 2334 }
3450 -
2335 +
3451 2336 // Disable buttons and show loading state
3452 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3453 - const sendBtn = getElementDOM(botId, 'send-button');
3454 - if (!uploadBtn) return;
2337 + const uploadBtn = document.getElementById('pdf-upload-btn');
2338 + const sendBtn = document.getElementById('send-button');
3455 2339 const originalBtnContent = uploadBtn.innerHTML;
3456 -
2340 +
3457 2341 try {
3458 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3459 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3460 2342 const formData = new FormData();
3461 2343 formData.append('action', 'mxchat_upload_pdf');
3462 2344 formData.append('pdf_file', file);
3463 2345 formData.append('session_id', sessionId);
3464 2346 formData.append('nonce', mxchatChat.nonce);
3465 -
2347 +
3466 2348 uploadBtn.disabled = true;
3467 - if (sendBtn) sendBtn.disabled = true;
2349 + sendBtn.disabled = true;
3468 2350 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3469 2351 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3470 2352 </svg>`;
3471 -
2353 +
3472 2354 const response = await fetch(mxchatChat.ajax_url, {
3473 2355 method: 'POST',
3474 2356 body: formData
3475 2357 });
3476 -
2358 +
3477 2359 const data = await response.json();
3478 -
2360 +
3479 2361 if (data.success) {
3480 2362 // Hide popular questions if they exist
3481 - if (hasQuickQuestions(botId)) {
3482 - collapseQuickQuestions(botId);
2363 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2364 + if (hasQuickQuestions()) {
2365 + collapseQuickQuestions();
3483 2366 }
3484 -
2367 +
3485 2368 // Show the active PDF name
3486 - showActivePdf(data.data.filename, botId);
3487 -
3488 - appendMessage('bot', data.data.message, '', [], false, botId);
3489 - scrollToBottom(botId);
3490 - instance.activePdfFile = data.data.filename;
2369 + showActivePdf(data.data.filename);
2370 +
2371 + appendMessage('bot', data.data.message);
2372 + scrollToBottom();
2373 + activePdfFile = data.data.filename;
3491 2374 } else {
2375 + //console.error('Upload failed:', data.data);
3492 2376 alert('Failed to upload PDF. Please try again.');
3493 2377 }
3494 2378 } catch (error) {
2379 + //console.error('Upload error:', error);
3495 2380 alert('Error uploading file. Please try again.');
3496 2381 } finally {
3497 2382 uploadBtn.disabled = false;
3498 - if (sendBtn) sendBtn.disabled = false;
2383 + sendBtn.disabled = false;
3499 2384 uploadBtn.innerHTML = originalBtnContent;
3500 2385 this.value = ''; // Reset file input
3501 2386 }
3502 2387 });
3503 2388
3504 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3505 - $(document).on('change', '.word-upload', async function(e) {
3506 - var botId = getBotIdFromElement(this);
3507 - var instance = MxChatInstances.get(botId);
3508 - const file = this.files[0];
3509 - const sessionId = MxChatInstances.ensureSession(botId);
3510 -
2389 + // Word file input change handler
2390 + addSafeEventListener('word-upload', 'change', async function(e) {
2391 + const file = e.target.files[0];
2392 +
3511 2393 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3512 2394 alert('Please select a valid Word document (.docx).');
3513 2395 return;
3514 2396 }
3515 -
2397 +
3516 2398 if (!sessionId) {
2399 + //console.error('No session ID found');
3517 2400 alert('Error: No session ID found');
3518 2401 return;
3519 2402 }
3520 -
3521 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3522 - alert('Error: Ajax configuration missing');
3523 - return;
3524 - }
3525 -
2403 +
3526 2404 // Disable buttons and show loading state
3527 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3528 - const sendBtn = getElementDOM(botId, 'send-button');
3529 - if (!uploadBtn) return;
2405 + const uploadBtn = document.getElementById('word-upload-btn');
2406 + const sendBtn = document.getElementById('send-button');
3530 2407 const originalBtnContent = uploadBtn.innerHTML;
3531 -
2408 +
3532 2409 try {
3533 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3534 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3535 2410 const formData = new FormData();
3536 2411 formData.append('action', 'mxchat_upload_word');
3537 2412 formData.append('word_file', file);
3538 2413 formData.append('session_id', sessionId);
3539 2414 formData.append('nonce', mxchatChat.nonce);
3540 -
2415 +
3541 2416 uploadBtn.disabled = true;
3542 - if (sendBtn) sendBtn.disabled = true;
2417 + sendBtn.disabled = true;
3543 2418 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3544 2419 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3545 2420 </svg>`;
3546 -
2421 +
3547 2422 const response = await fetch(mxchatChat.ajax_url, {
3548 2423 method: 'POST',
3549 2424 body: formData
3550 2425 });
3551 -
2426 +
3552 2427 const data = await response.json();
3553 -
2428 +
3554 2429 if (data.success) {
3555 2430 // Hide popular questions if they exist
3556 - if (hasQuickQuestions(botId)) {
3557 - collapseQuickQuestions(botId);
2431 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2432 + if (hasQuickQuestions()) {
2433 + collapseQuickQuestions();
3558 2434 }
3559 -
2435 +
3560 2436 // Show the active Word document name
3561 - showActiveWord(data.data.filename, botId);
3562 -
3563 - appendMessage('bot', data.data.message, '', [], false, botId);
3564 - scrollToBottom(botId);
3565 - instance.activeWordFile = data.data.filename;
2437 + showActiveWord(data.data.filename);
2438 +
2439 + appendMessage('bot', data.data.message);
2440 + scrollToBottom();
2441 + activeWordFile = data.data.filename;
3566 2442 } else {
2443 + //console.error('Upload failed:', data.data);
3567 2444 alert('Failed to upload Word document. Please try again.');
3568 2445 }
3569 2446 } catch (error) {
2447 + //console.error('Upload error:', error);
3570 2448 alert('Error uploading file. Please try again.');
3571 2449 } finally {
3572 2450 uploadBtn.disabled = false;
3573 - if (sendBtn) sendBtn.disabled = false;
2451 + sendBtn.disabled = false;
3574 2452 uploadBtn.innerHTML = originalBtnContent;
3575 2453 this.value = ''; // Reset file input
3576 2454 }
3577 2455 });
3578 2456
3579 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3580 - $(document).on('click', '.remove-pdf-btn', function(e) {
2457 + // Remove button click handlers
2458 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3581 2459 e.preventDefault();
3582 2460 e.stopPropagation();
3583 - removeActivePdf(getBotIdFromElement(this));
2461 + removeActivePdf();
3584 2462 });
3585 -
3586 - $(document).on('click', '.remove-word-btn', function(e) {
2463 +
2464 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3587 2465 e.preventDefault();
3588 2466 e.stopPropagation();
3589 - removeActiveWord(getBotIdFromElement(this));
2467 + removeActiveWord();
3590 2468 });
3591 2469
3592 2470 // Window resize handlers
3593 2471 $(window).on('resize orientationchange', function() {
@@ -3625,497 +2503,454 @@
3625 2503 });
3626 2504
3627 2505
3628 2506 // ====================================
3629 -// INIT LOADER & CHAT CONTAINER HELPERS
2507 +// EMAIL COLLECTION SETUP - FIXED VERSION
3630 2508 // ====================================
3631 -// These must be outside the email collection block so they're always available
3632 -// (used by persistence loading even when email collection is off)
3633 -
3634 -function showInitLoader(botId) {
3635 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3636 - if (loader) loader.style.display = 'flex';
3637 -}
3638 -
3639 -function hideInitLoader(botId) {
3640 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3641 - if (loader) loader.style.display = 'none';
3642 -}
3643 -
3644 -function showEmailFormForBot(botId) {
3645 - hideInitLoader(botId);
3646 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3647 - var chatContainer = getElementDOM(botId, 'chat-container');
3648 - if (emailBlocker) emailBlocker.style.display = 'flex';
3649 - if (chatContainer) chatContainer.style.display = 'none';
3650 -}
3651 -
3652 -function showChatContainerForBot(botId) {
3653 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3654 - var chatContainer = getElementDOM(botId, 'chat-container');
3655 - if (emailBlocker) emailBlocker.style.display = 'none';
3656 -
3657 - var instance = MxChatInstances.get(botId);
3658 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3659 -
3660 - // If persistence is on and history hasn't loaded yet, show loader
3661 - // while history loads to prevent flash of empty chat
3662 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3663 - if (chatContainer) chatContainer.style.display = 'none';
3664 - showInitLoader(botId);
3665 - loadChatHistory(botId, function() {
3666 - hideInitLoader(botId);
3667 - if (chatContainer) chatContainer.style.display = 'flex';
3668 - scrollToBottom(botId, true);
3669 - });
3670 - } else {
3671 - hideInitLoader(botId);
3672 - if (chatContainer) chatContainer.style.display = 'flex';
3673 - if (typeof loadChatHistory === 'function') {
3674 - loadChatHistory(botId);
3675 - }
3676 - }
3677 -}
3678 -
3679 -// ====================================
3680 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3681 -// ====================================
3682 2509 // Only run email collection setup if it's enabled
3683 2510 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2511 + //console.log('Email collection is enabled, setting up handlers...');
2512 +
2513 + // Email collection form setup and handlers
2514 + const emailForm = document.getElementById('email-collection-form');
2515 + const emailBlocker = document.getElementById('email-blocker');
2516 + const chatbotWrapper = document.getElementById('chat-container');
3684 2517
3685 - // Track submitting state per bot
3686 - const emailSubmittingState = {};
2518 + if (emailForm && emailBlocker && chatbotWrapper) {
2519 +
2520 + // Add loading state management
2521 + let isSubmitting = false;
2522 +
2523 + // Optimized UI transition functions
2524 + function showEmailForm() {
2525 + emailBlocker.style.display = 'flex';
2526 + chatbotWrapper.style.display = 'none';
2527 + }
3687 2528
3688 - // Add CSS animations for email form (once globally)
3689 - if (!document.getElementById('email-error-styles')) {
3690 - const style = document.createElement('style');
3691 - style.id = 'email-error-styles';
3692 - style.textContent = `
3693 - @keyframes fadeInError {
3694 - from { opacity: 0; transform: translateY(-5px); }
3695 - to { opacity: 1; transform: translateY(0); }
2529 + function showChatContainer() {
2530 + // Show chat immediately without delay
2531 + emailBlocker.style.display = 'none';
2532 + chatbotWrapper.style.display = 'flex';
2533 +
2534 + // Load chat history only after showing chat container
2535 + if (typeof loadChatHistory === 'function') {
2536 + loadChatHistory();
3696 2537 }
3697 - .email-input-shake {
3698 - animation: shake 0.5s ease-in-out;
3699 - }
3700 - @keyframes shake {
3701 - 0%, 100% { transform: translateX(0); }
3702 - 25% { transform: translateX(-5px); }
3703 - 75% { transform: translateX(5px); }
3704 - }
3705 - @keyframes spin {
3706 - from { transform: rotate(0deg); }
3707 - to { transform: rotate(360deg); }
3708 - }
3709 - .email-spinner {
3710 - display: inline-block;
3711 - vertical-align: middle;
3712 - }
3713 - `;
3714 - document.head.appendChild(style);
3715 - }
2538 + }
3716 2539
3717 - function isValidEmailAddress(email) {
3718 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3719 - return emailRegex.test(email.trim()) && email.length <= 254;
3720 - }
2540 + // Enhanced email validation
2541 + function isValidEmail(email) {
2542 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2543 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2544 + }
3721 2545
3722 - function isValidNameInput(name) {
3723 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3724 - }
2546 + // Enhanced name validation
2547 + function isValidName(name) {
2548 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2549 + }
3725 2550
3726 - /**
3727 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3728 - * @param {string} botId - The bot instance ID
3729 - * @param {string} visitorName - The visitor's name to insert
3730 - */
3731 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3732 - var chatBox = getElementDOM(botId, 'chat-box');
3733 - if (!chatBox) return;
3734 -
3735 - // Find the first bot message (intro message)
3736 - var introMessage = chatBox.querySelector('.bot-message');
3737 - if (!introMessage) return;
3738 -
3739 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3740 - if (!messageContent) return;
3741 -
3742 - var html = messageContent.innerHTML;
3743 -
3744 - // Replace {visitor_name} placeholder (case-insensitive)
3745 - if (visitorName && visitorName.trim()) {
3746 - // Escape HTML to prevent XSS
3747 - var safeName = $('<div>').text(visitorName.trim()).html();
3748 - html = html.replace(/\{visitor_name\}/gi, safeName);
3749 - } else {
3750 - // Remove placeholder and clean up spacing if no name provided
3751 - html = html.replace(/\{visitor_name\}/gi, '');
3752 - // Clean up any double spaces that might result
3753 - html = html.replace(/\s{2,}/g, ' ').trim();
2551 + // Show loading state with spinner
2552 + function setSubmissionState(loading) {
2553 + const submitButton = document.getElementById('email-submit-button');
2554 + const emailInput = document.getElementById('user-email');
2555 + const nameInput = document.getElementById('user-name');
2556 +
2557 + if (loading) {
2558 + isSubmitting = true;
2559 + if (submitButton) submitButton.disabled = true;
2560 + if (emailInput) emailInput.disabled = true;
2561 + if (nameInput) nameInput.disabled = true;
2562 +
2563 + // Store original content and add spinner
2564 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2565 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2566 +
2567 + // Add loading spinner while keeping original text
2568 + const originalText = submitButton.textContent;
2569 + submitButton.innerHTML = `
2570 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2571 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2572 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2573 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2574 + </circle>
2575 + </svg>
2576 + ${originalText}
2577 + `;
2578 +
2579 + submitButton.style.opacity = '0.8';
2580 + }
2581 + } else {
2582 + isSubmitting = false;
2583 + if (submitButton) submitButton.disabled = false;
2584 + if (emailInput) emailInput.disabled = false;
2585 + if (nameInput) nameInput.disabled = false;
2586 +
2587 + // Restore original content
2588 + if (submitButton) {
2589 + const originalHtml = submitButton.getAttribute('data-original-html');
2590 + if (originalHtml) {
2591 + submitButton.innerHTML = originalHtml;
2592 + }
2593 + submitButton.style.opacity = '1';
2594 + }
2595 + }
3754 2596 }
3755 2597
3756 - messageContent.innerHTML = html;
3757 - }
3758 -
3759 - function setEmailSubmissionState(botId, loading) {
3760 - var submitButton = getElementDOM(botId, 'email-submit-button');
3761 - var emailInput = getElementDOM(botId, 'user-email');
3762 - var nameInput = getElementDOM(botId, 'user-name');
3763 -
3764 - if (loading) {
3765 - emailSubmittingState[botId] = true;
3766 - if (submitButton) submitButton.disabled = true;
3767 - if (emailInput) emailInput.disabled = true;
3768 - if (nameInput) nameInput.disabled = true;
3769 -
3770 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3771 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3772 - const originalText = submitButton.textContent;
3773 - submitButton.innerHTML = `
3774 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3775 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3776 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3777 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3778 - </circle>
3779 - </svg>
3780 - ${originalText}
2598 + // Error display functions
2599 + function showEmailError(message) {
2600 + clearEmailError();
2601 +
2602 + const errorDiv = document.createElement('div');
2603 + errorDiv.className = 'email-error';
2604 + errorDiv.style.cssText = `
2605 + color: #e74c3c;
2606 + font-size: 12px;
2607 + margin-top: 8px;
2608 + padding: 4px 0;
2609 + animation: fadeInError 0.3s ease;
2610 + `;
2611 + errorDiv.textContent = message;
2612 +
2613 + // Add CSS animation if not already present
2614 + if (!document.getElementById('email-error-styles')) {
2615 + const style = document.createElement('style');
2616 + style.id = 'email-error-styles';
2617 + style.textContent = `
2618 + @keyframes fadeInError {
2619 + from { opacity: 0; transform: translateY(-5px); }
2620 + to { opacity: 1; transform: translateY(0); }
2621 + }
2622 + .email-input-shake {
2623 + animation: shake 0.5s ease-in-out;
2624 + }
2625 + @keyframes shake {
2626 + 0%, 100% { transform: translateX(0); }
2627 + 25% { transform: translateX(-5px); }
2628 + 75% { transform: translateX(5px); }
2629 + }
2630 + @keyframes spin {
2631 + from { transform: rotate(0deg); }
2632 + to { transform: rotate(360deg); }
2633 + }
2634 + .email-spinner {
2635 + display: inline-block;
2636 + vertical-align: middle;
2637 + }
3781 2638 `;
3782 - submitButton.style.opacity = '0.8';
2639 + document.head.appendChild(style);
3783 2640 }
3784 - } else {
3785 - emailSubmittingState[botId] = false;
3786 - if (submitButton) submitButton.disabled = false;
3787 - if (emailInput) emailInput.disabled = false;
3788 - if (nameInput) nameInput.disabled = false;
3789 -
3790 - if (submitButton) {
3791 - const originalHtml = submitButton.getAttribute('data-original-html');
3792 - if (originalHtml) {
3793 - submitButton.innerHTML = originalHtml;
3794 - }
3795 - submitButton.style.opacity = '1';
2641 +
2642 + emailForm.appendChild(errorDiv);
2643 +
2644 + // Add shake animation to inputs
2645 + const emailInput = document.getElementById('user-email');
2646 + const nameInput = document.getElementById('user-name');
2647 +
2648 + if (emailInput) {
2649 + emailInput.classList.add('email-input-shake');
2650 + setTimeout(() => {
2651 + emailInput.classList.remove('email-input-shake');
2652 + }, 500);
3796 2653 }
2654 +
2655 + if (nameInput) {
2656 + nameInput.classList.add('email-input-shake');
2657 + setTimeout(() => {
2658 + nameInput.classList.remove('email-input-shake');
2659 + }, 500);
2660 + }
3797 2661 }
3798 - }
3799 2662
3800 - function showEmailError(botId, message) {
3801 - clearEmailError(botId);
3802 -
3803 - var emailForm = getElementDOM(botId, 'email-collection-form');
3804 - if (!emailForm) return;
3805 -
3806 - const errorDiv = document.createElement('div');
3807 - errorDiv.className = 'email-error';
3808 - errorDiv.style.cssText = `
3809 - color: #e74c3c;
3810 - font-size: 12px;
3811 - margin-top: 8px;
3812 - padding: 4px 0;
3813 - animation: fadeInError 0.3s ease;
3814 - `;
3815 - errorDiv.textContent = message;
3816 - emailForm.appendChild(errorDiv);
3817 -
3818 - // Add shake animation to inputs
3819 - var emailInput = getElementDOM(botId, 'user-email');
3820 - var nameInput = getElementDOM(botId, 'user-name');
3821 -
3822 - if (emailInput) {
3823 - emailInput.classList.add('email-input-shake');
3824 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3825 - }
3826 - if (nameInput) {
3827 - nameInput.classList.add('email-input-shake');
3828 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3829 - }
3830 - }
3831 -
3832 - function clearEmailError(botId) {
3833 - var emailForm = getElementDOM(botId, 'email-collection-form');
3834 - if (emailForm) {
2663 + function clearEmailError() {
3835 2664 const existingErrors = emailForm.querySelectorAll('.email-error');
3836 2665 existingErrors.forEach(error => error.remove());
3837 2666 }
3838 - }
3839 2667
3840 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3841 - function resolveEmailState(botId) {
3842 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3843 - if (mxchatChat.initial_email_state.show_email_form) {
3844 - showEmailFormForBot(botId);
3845 - } else {
3846 - showChatContainerForBot(botId);
2668 + // MAIN FORM SUBMIT HANDLER - This is the critical fix
2669 + //console.log('Setting up form submit handler...');
2670 +
2671 + // Remove any existing event listeners first
2672 + emailForm.removeEventListener('submit', handleFormSubmit);
2673 +
2674 + // Add the form submit handler
2675 + emailForm.addEventListener('submit', handleFormSubmit);
2676 +
2677 + function handleFormSubmit(event) {
2678 + //console.log('Form submit handler triggered!');
2679 + event.preventDefault();
2680 + event.stopPropagation();
2681 +
2682 + // Prevent double submission
2683 + if (isSubmitting) {
2684 + //console.log('Already submitting, ignoring...');
2685 + return false;
3847 2686 }
3848 - } else {
3849 - checkSessionAndEmailForBot(botId);
3850 - }
3851 - }
3852 2687
3853 - function checkSessionAndEmailForBot(botId) {
3854 - const sessionId = MxChatInstances.ensureSession(botId);
2688 + const userEmail = document.getElementById('user-email').value.trim();
2689 + const nameInput = document.getElementById('user-name');
2690 + const userName = nameInput ? nameInput.value.trim() : '';
2691 + const sessionId = getChatSession();
3855 2692
3856 - // Hide both panels while we check — show loader instead
3857 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3858 - var chatContainer = getElementDOM(botId, 'chat-container');
3859 - if (emailBlocker) emailBlocker.style.display = 'none';
3860 - if (chatContainer) chatContainer.style.display = 'none';
3861 - showInitLoader(botId);
2693 + //console.log('Form data:', { userEmail, userName, sessionId });
3862 2694
3863 - fetch(mxchatChat.ajax_url, {
3864 - method: 'POST',
3865 - headers: {
3866 - 'Content-Type': 'application/x-www-form-urlencoded',
3867 - },
3868 - body: new URLSearchParams({
3869 - action: 'mxchat_check_email_provided',
3870 - session_id: sessionId,
3871 - nonce: mxchatChat.nonce,
3872 - })
3873 - })
3874 - .then((response) => {
3875 - if (!response.ok) {
3876 - throw new Error(`HTTP error! status: ${response.status}`);
2695 + // Validate email before submission
2696 + if (!userEmail) {
2697 + showEmailError('Please enter your email address.');
2698 + return false;
3877 2699 }
3878 - return response.json();
3879 - })
3880 - .then((data) => {
3881 - if (data.success) {
3882 - if (data.data.logged_in || data.data.email) {
3883 - showChatContainerForBot(botId);
3884 - } else {
3885 - showEmailFormForBot(botId);
3886 - }
3887 - } else {
3888 - showEmailFormForBot(botId);
2700 +
2701 + if (!isValidEmail(userEmail)) {
2702 + showEmailError('Please enter a valid email address.');
2703 + return false;
3889 2704 }
3890 - })
3891 - .catch((error) => {
3892 - showEmailFormForBot(botId);
3893 - });
3894 - }
3895 2705
3896 - // Event delegation for email form submission
3897 - $(document).on('submit', '.email-collection-form', function(e) {
3898 - e.preventDefault();
3899 - e.stopPropagation();
2706 + // Validate name if field exists
2707 + if (nameInput && !isValidName(userName)) {
2708 + showEmailError('Please enter a valid name (2-100 characters).');
2709 + return false;
2710 + }
3900 2711
3901 - var botId = getBotIdFromElement(this);
2712 + // Clear any existing errors
2713 + clearEmailError();
2714 + setSubmissionState(true);
3902 2715
3903 - // Prevent double submission
3904 - if (emailSubmittingState[botId]) {
3905 - return false;
3906 - }
2716 + //console.log('Sending AJAX request...');
3907 2717
3908 - var emailInput = getElementDOM(botId, 'user-email');
3909 - var nameInput = getElementDOM(botId, 'user-name');
3910 - var userEmail = emailInput ? emailInput.value.trim() : '';
3911 - var userName = nameInput ? nameInput.value.trim() : '';
3912 - var sessionId = MxChatInstances.ensureSession(botId);
2718 + // Prepare form data with optional name
2719 + const formData = new URLSearchParams({
2720 + action: 'mxchat_handle_save_email_and_response',
2721 + email: userEmail,
2722 + session_id: sessionId,
2723 + nonce: mxchatChat.nonce,
2724 + });
3913 2725
3914 - // Validate email
3915 - if (!userEmail) {
3916 - showEmailError(botId, 'Please enter your email address.');
3917 - return false;
3918 - }
2726 + // Add name to form data if provided
2727 + if (userName) {
2728 + formData.append('name', userName);
2729 + }
3919 2730
3920 - if (!isValidEmailAddress(userEmail)) {
3921 - showEmailError(botId, 'Please enter a valid email address.');
3922 - return false;
3923 - }
2731 + fetch(mxchatChat.ajax_url, {
2732 + method: 'POST',
2733 + headers: {
2734 + 'Content-Type': 'application/x-www-form-urlencoded',
2735 + },
2736 + body: formData
2737 + })
2738 + .then((response) => {
2739 + //console.log('Response received:', response);
2740 + if (!response.ok) {
2741 + throw new Error(`HTTP error! status: ${response.status}`);
2742 + }
2743 + return response.json();
2744 + })
2745 + .then((data) => {
2746 + //console.log('Response data:', data);
2747 + setSubmissionState(false);
2748 +
2749 + if (data.success) {
2750 + // Show chat immediately
2751 + showChatContainer();
3924 2752
3925 - // Validate name if field exists and has content
3926 - if (nameInput && userName && !isValidNameInput(userName)) {
3927 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3928 - return false;
2753 + // Handle bot response if provided
2754 + if (data.message && typeof appendMessage === 'function') {
2755 + setTimeout(() => {
2756 + appendMessage('bot', data.message);
2757 + if (typeof scrollToBottom === 'function') {
2758 + scrollToBottom();
2759 + }
2760 + }, 100);
2761 + }
2762 + } else {
2763 + showEmailError(data.message || 'Failed to save email. Please try again.');
2764 + }
2765 + })
2766 + .catch((error) => {
2767 + //console.error('Email submission error:', error);
2768 + setSubmissionState(false);
2769 + showEmailError('An error occurred. Please try again.');
2770 + });
2771 +
2772 + return false; // Extra prevention
3929 2773 }
3930 2774
3931 - clearEmailError(botId);
3932 - setEmailSubmissionState(botId, true);
2775 + // Real-time email validation
2776 + const emailInput = document.getElementById('user-email');
2777 + if (emailInput) {
2778 + let validationTimeout;
2779 +
2780 + emailInput.addEventListener('input', function() {
2781 + // Clear previous validation timeout
2782 + if (validationTimeout) {
2783 + clearTimeout(validationTimeout);
2784 + }
2785 +
2786 + // Debounce validation
2787 + validationTimeout = setTimeout(() => {
2788 + const email = this.value.trim();
2789 + clearEmailError();
2790 +
2791 + if (email && !isValidEmail(email)) {
2792 + showEmailError('Please enter a valid email address.');
2793 + }
2794 + }, 500);
2795 + });
3933 2796
3934 - // Prepare form data
3935 - const formData = new URLSearchParams({
3936 - action: 'mxchat_handle_save_email_and_response',
3937 - email: userEmail,
3938 - session_id: sessionId,
3939 - nonce: mxchatChat.nonce,
3940 - });
3941 -
3942 - if (userName) {
3943 - formData.append('name', userName);
2797 + // Handle Enter key
2798 + emailInput.addEventListener('keypress', function(e) {
2799 + if (e.key === 'Enter' && !isSubmitting) {
2800 + e.preventDefault();
2801 + emailForm.dispatchEvent(new Event('submit'));
2802 + }
2803 + });
3944 2804 }
3945 2805
3946 - fetch(mxchatChat.ajax_url, {
3947 - method: 'POST',
3948 - headers: {
3949 - 'Content-Type': 'application/x-www-form-urlencoded',
3950 - },
3951 - body: formData
3952 - })
3953 - .then((response) => {
3954 - if (!response.ok) {
3955 - throw new Error(`HTTP error! status: ${response.status}`);
3956 - }
3957 - return response.json();
3958 - })
3959 - .then((data) => {
3960 - setEmailSubmissionState(botId, false);
3961 -
3962 - if (data.success) {
3963 - showChatContainerForBot(botId);
3964 -
3965 - // Replace {visitor_name} placeholder in intro message with actual name
3966 - if (userName) {
3967 - replaceVisitorNamePlaceholder(botId, userName);
3968 - } else {
3969 - // Remove placeholder if no name provided
3970 - replaceVisitorNamePlaceholder(botId, '');
2806 + // Real-time name validation
2807 + const nameInput = document.getElementById('user-name');
2808 + if (nameInput) {
2809 + let nameValidationTimeout;
2810 +
2811 + nameInput.addEventListener('input', function() {
2812 + // Clear previous validation timeout
2813 + if (nameValidationTimeout) {
2814 + clearTimeout(nameValidationTimeout);
3971 2815 }
2816 +
2817 + // Debounce validation
2818 + nameValidationTimeout = setTimeout(() => {
2819 + const name = this.value.trim();
2820 + clearEmailError();
2821 +
2822 + if (name && !isValidName(name)) {
2823 + showEmailError('Name must be between 2 and 100 characters.');
2824 + }
2825 + }, 500);
2826 + });
3972 2827
3973 - if (data.message && typeof appendMessage === 'function') {
3974 - setTimeout(() => {
3975 - appendMessage('bot', data.message, '', [], false, botId);
3976 - if (typeof scrollToBottom === 'function') {
3977 - scrollToBottom(botId);
3978 - }
3979 - }, 100);
2828 + // Handle Enter key
2829 + nameInput.addEventListener('keypress', function(e) {
2830 + if (e.key === 'Enter' && !isSubmitting) {
2831 + e.preventDefault();
2832 + emailForm.dispatchEvent(new Event('submit'));
3980 2833 }
3981 - } else {
3982 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
3983 - }
3984 - })
3985 - .catch((error) => {
3986 - setEmailSubmissionState(botId, false);
3987 - showEmailError(botId, 'An error occurred. Please try again.');
3988 - });
3989 -
3990 - return false;
3991 - });
3992 -
3993 - // Real-time email validation using event delegation
3994 - $(document).on('input', '.mxchat-email-input', function() {
3995 - var botId = getBotIdFromElement(this);
3996 - var $input = $(this);
3997 -
3998 - // Clear previous timeout
3999 - clearTimeout($input.data('validationTimeout'));
4000 -
4001 - // Debounce validation
4002 - var timeout = setTimeout(() => {
4003 - var email = this.value.trim();
4004 - clearEmailError(botId);
4005 -
4006 - if (email && !isValidEmailAddress(email)) {
4007 - showEmailError(botId, 'Please enter a valid email address.');
4008 - }
4009 - }, 500);
4010 -
4011 - $input.data('validationTimeout', timeout);
4012 - });
4013 -
4014 - // Handle Enter key in email input
4015 - $(document).on('keypress', '.mxchat-email-input', function(e) {
4016 - if (e.key === 'Enter') {
4017 - e.preventDefault();
4018 - var botId = getBotIdFromElement(this);
4019 - if (!emailSubmittingState[botId]) {
4020 - $(this).closest('.email-collection-form').submit();
4021 - }
2834 + });
4022 2835 }
4023 - });
4024 2836
4025 - // Handle Enter key in name input
4026 - $(document).on('keypress', '.mxchat-name-input', function(e) {
4027 - if (e.key === 'Enter') {
4028 - e.preventDefault();
4029 - var botId = getBotIdFromElement(this);
4030 - if (!emailSubmittingState[botId]) {
4031 - $(this).closest('.email-collection-form').submit();
2837 + // Initial state check
2838 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2839 + //console.log('Using server-provided email state:', mxchatChat.initial_email_state);
2840 +
2841 + const emailState = mxchatChat.initial_email_state;
2842 + if (emailState.show_email_form) {
2843 + showEmailForm();
2844 + } else {
2845 + showChatContainer();
4032 2846 }
2847 + } else {
2848 + // Check email status via AJAX
2849 + setTimeout(checkSessionAndEmail, 100);
4033 2850 }
4034 - });
4035 2851
4036 - // Initialize email check for all bot instances
4037 - // For floating bots: defer until widget is opened (zero passive AJAX)
4038 - // For embedded bots: check immediately since the form is visible
4039 - $('.mxchat-chatbot-wrapper').each(function() {
4040 - var botId = $(this).data('bot-id') || 'default';
4041 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4042 -
4043 - if (emailBlocker) {
4044 - if (isEmbeddedBot(botId)) {
4045 - // Embedded bots are always visible — check now
4046 - resolveEmailState(botId);
4047 - }
4048 - // Floating bots: handled in the widget open handler
4049 - } else if (isEmbeddedBot(botId)) {
4050 - // Embedded bot, no email collection — load history with loader
4051 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4052 - if (chatPersistenceEnabled) {
4053 - MxChatInstances.ensureSession(botId);
4054 - showChatContainerForBot(botId);
4055 - }
2852 + // Check if email exists for the current session
2853 + function checkSessionAndEmail() {
2854 + const sessionId = getChatSession();
2855 +
2856 + fetch(mxchatChat.ajax_url, {
2857 + method: 'POST',
2858 + headers: {
2859 + 'Content-Type': 'application/x-www-form-urlencoded',
2860 + },
2861 + body: new URLSearchParams({
2862 + action: 'mxchat_check_email_provided',
2863 + session_id: sessionId,
2864 + nonce: mxchatChat.nonce,
2865 + })
2866 + })
2867 + .then((response) => {
2868 + if (!response.ok) {
2869 + throw new Error(`HTTP error! status: ${response.status}`);
2870 + }
2871 + return response.json();
2872 + })
2873 + .then((data) => {
2874 + if (data.success) {
2875 + if (data.data.logged_in || data.data.email) {
2876 + showChatContainer();
2877 + } else {
2878 + showEmailForm();
2879 + }
2880 + } else {
2881 + // On error, default to showing email form
2882 + showEmailForm();
2883 + }
2884 + })
2885 + .catch((error) => {
2886 + //console.warn('Email check failed, defaulting to email form:', error);
2887 + showEmailForm();
2888 + });
4056 2889 }
4057 - });
2890 +
2891 + } else {
2892 + console.error('Email collection is enabled but essential elements are missing:', {
2893 + emailForm: !!emailForm,
2894 + emailBlocker: !!emailBlocker,
2895 + chatbotWrapper: !!chatbotWrapper
2896 + });
2897 + }
2898 +} else {
2899 + //console.log('Email collection is disabled');
4058 2900 }
4059 2901
4060 - // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
4061 - $(document).on('click', '.pre-chat-message', function() {
4062 - var botId = getBotIdFromElement(this);
4063 - var $chatbot = getElement(botId, 'floating-chatbot');
4064 - if ($chatbot.hasClass('hidden')) {
4065 - $chatbot.removeClass('hidden').addClass('visible');
4066 - getElement(botId, 'floating-chatbot-button').addClass('hidden');
4067 - handlePreChatDismissal(botId);
2902 + // Open chatbot when pre-chat message is clicked
2903 + $(document).on('click', '#pre-chat-message', function() {
2904 + var chatbot = $('#floating-chatbot');
2905 + if (chatbot.hasClass('hidden')) {
2906 + chatbot.removeClass('hidden').addClass('visible');
2907 + $('#floating-chatbot-button').addClass('hidden');
2908 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
4068 2909 disableScroll(); // Disable scroll when chatbot opens
4069 -
4070 - // Load chat history for returning visitors (persistence)
4071 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4072 - if (chatPersistenceEnabled) {
4073 - MxChatInstances.ensureSession(botId);
4074 - }
4075 -
4076 - // Deferred email check — only on first widget open
4077 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4078 - var instance = MxChatInstances.get(botId);
4079 - if (emailBlocker && !instance.emailCheckDone) {
4080 - instance.emailCheckDone = true;
4081 - resolveEmailState(botId);
4082 - } else if (!emailBlocker) {
4083 - showChatContainerForBot(botId);
4084 - }
4085 2910 }
4086 2911 });
4087 2912
4088 - // Legacy duplicate close handler removed — handled by single event delegation above
2913 + var closeButton = document.querySelector('.close-pre-chat-message');
2914 + if (closeButton) {
2915 + closeButton.addEventListener('click', function() {
2916 + $('#pre-chat-message').fadeOut(200); // Hide the message
4089 2917
2918 + // Send an AJAX request to set the transient flag for 24 hours
2919 + $.ajax({
2920 + url: mxchatChat.ajax_url,
2921 + type: 'POST',
2922 + data: {
2923 + action: 'mxchat_dismiss_pre_chat_message',
2924 + _ajax_nonce: mxchatChat.nonce
2925 + },
2926 + success: function() {
2927 + //console.log('Pre-chat message dismissed for 24 hours.');
4090 2928
4091 -function hasQuickQuestions(botId) {
4092 - botId = botId || 'default';
4093 - var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4094 - if (!questionsContainer) return false;
4095 - const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
2929 + // Ensure the message is hidden after dismissal
2930 + $('#pre-chat-message').hide();
2931 + },
2932 + error: function() {
2933 + //console.error('Failed to dismiss pre-chat message.');
2934 + }
2935 + });
2936 + });
2937 + }
2938 +
2939 +
2940 +function hasQuickQuestions() {
2941 + const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
4096 2942 return questionButtons.length > 0;
4097 2943 }
4098 2944
4099 -/**
4100 - * Check if a bot is embedded (not floating)
4101 - * Embedded bots don't have a .floating-chatbot wrapper
4102 - */
4103 -function isEmbeddedBot(botId) {
4104 - botId = botId || 'default';
4105 - var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
4106 - return !floatingWrapper;
4107 -}
4108 -
4109 -function collapseQuickQuestions(botId) {
4110 - botId = botId || 'default';
4111 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4112 - if (questionsContainer && hasQuickQuestions(botId)) {
2945 +function collapseQuickQuestions() {
2946 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2947 + if (questionsContainer && hasQuickQuestions()) {
4113 2948 questionsContainer.classList.add('collapsed');
4114 2949 questionsContainer.classList.add('has-been-collapsed');
4115 2950 try {
4116 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
4117 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
2951 + sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2952 + sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
4118 2953 } catch (e) {
4119 2954 // Ignore if sessionStorage is not available
4120 2955 }
4121 2956 }
@@ -4120,15 +2955,14 @@
4120 2955 }
4121 2956 }
4122 2957 }
4123 2958
4124 -function expandQuickQuestions(botId) {
4125 - botId = botId || 'default';
4126 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4127 - if (questionsContainer && hasQuickQuestions(botId)) {
2959 +function expandQuickQuestions() {
2960 + const questionsContainer = document.getElementById('mxchat-popular-questions');
2961 + if (questionsContainer && hasQuickQuestions()) {
4128 2962 questionsContainer.classList.remove('collapsed');
4129 2963 try {
4130 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
2964 + sessionStorage.setItem('mxchat_questions_collapsed', 'false');
4131 2965 } catch (e) {
4132 2966 // Ignore if sessionStorage is not available
4133 2967 }
4134 2968 }
@@ -4133,24 +2967,18 @@
4133 2967 }
4134 2968 }
4135 2969 }
4136 2970
4137 -function checkQuickQuestionsState(botId) {
4138 - botId = botId || 'default';
4139 - if (!hasQuickQuestions(botId)) {
2971 +function checkQuickQuestionsState() {
2972 + if (!hasQuickQuestions()) {
4140 2973 return; // Don't do anything if no questions exist
4141 2974 }
4142 -
4143 - // Skip restoring collapsed state for embedded bots - they should always start expanded
4144 - if (isEmbeddedBot(botId)) {
4145 - return;
4146 - }
4147 -
2975 +
4148 2976 try {
4149 - const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
4150 - const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
4151 -
4152 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
2977 + const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2978 + const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2979 +
2980 + const questionsContainer = document.getElementById('mxchat-popular-questions');
4153 2981 if (questionsContainer) {
4154 2982 if (hasBeenCollapsed === 'true') {
4155 2983 questionsContainer.classList.add('has-been-collapsed');
4156 2984 }
@@ -4163,36 +2991,32 @@
4163 2991 }
4164 2992 }
4165 2993
4166 2994 // Global delegation for dynamically added links as fallback
4167 -// Use class selector for multi-instance support
4168 -$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
2995 +$(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
4169 2996 const $link = $(this);
4170 2997 const messageDiv = $link.closest('.bot-message, .agent-message');
4171 -
2998 +
4172 2999 // Only process bot/agent message links
4173 3000 if (messageDiv.length > 0) {
4174 3001 const originalHref = $link.attr('href');
4175 -
3002 +
4176 3003 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
4177 3004 e.preventDefault();
4178 3005 e.stopPropagation();
4179 -
3006 +
4180 3007 // Mark as tracked
4181 3008 $link.attr('data-tracked', 'true');
4182 -
4183 - // Get bot ID from the chat box context
4184 - var botId = getBotIdFromElement(this);
4185 -
3009 +
4186 3010 // Get message context from the message div
4187 3011 const messageText = messageDiv.text().substring(0, 200);
4188 -
3012 +
4189 3013 $.ajax({
4190 3014 url: mxchatChat.ajax_url,
4191 3015 type: 'POST',
4192 3016 data: {
4193 3017 action: 'mxchat_track_url_click',
4194 - session_id: getChatSession(botId),
3018 + session_id: getChatSession(),
4195 3019 url: originalHref,
4196 3020 message_context: messageText,
4197 3021 nonce: mxchatChat.nonce
4198 3022 },
@@ -4203,58 +3027,50 @@
4203 3027 window.location.href = originalHref;
4204 3028 }
4205 3029 }
4206 3030 });
4207 -
3031 +
4208 3032 return false;
4209 3033 }
4210 3034 }
4211 3035 });
4212 3036
4213 - // ====================================
4214 - // MAIN INITIALIZATION
4215 - // ====================================
3037 +
3038 +
3039 +
3040 +// ====================================
3041 +// MAIN INITIALIZATION
3042 +// ====================================
4216 3043
4217 - // Initialize all chatbot instances on the page
4218 - initializeAllInstances();
3044 +if ($('#floating-chatbot').hasClass('hidden')) {
3045 + $('#floating-chatbot-button').removeClass('hidden');
3046 +}
3047 +// Initialize when document is ready
3048 +setFullHeight();
3049 +trackOriginatingPage();
4219 3050
4220 - // Legacy initialization for single bot compatibility
4221 - $('.floating-chatbot.hidden').each(function() {
4222 - var botId = getBotIdFromElement(this);
4223 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
4224 - });
3051 +// Only load chat history if email collection is disabled
3052 +if (mxchatChat.email_collection_enabled !== 'on') {
3053 + loadChatHistory();
3054 +}
4225 3055
4226 - // Initialize when document is ready
4227 - setFullHeight();
3056 +initializeChatVisibility();
4228 3057
4229 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4230 - // until the user's first interaction via MxChatInstances.ensureSession()
3058 +// Make functions globally available for add-ons
3059 +window.hasQuickQuestions = hasQuickQuestions;
3060 +window.collapseQuickQuestions = collapseQuickQuestions;
3061 +window.appendMessage = appendMessage;
3062 +window.appendThinkingMessage = appendThinkingMessage;
3063 +window.scrollToBottom = scrollToBottom;
3064 +window.scrollElementToTop = scrollElementToTop;
3065 +window.replaceLastMessage = replaceLastMessage;
3066 +window.callMxChat = callMxChat;
3067 +window.callMxChatStream = callMxChatStream;
3068 +window.shouldUseStreaming = shouldUseStreaming;
3069 +window.getChatSession = getChatSession;
3070 +window.getPageContext = getPageContext;
3071 +window.updateStreamingMessage = updateStreamingMessage;
4231 3072
4232 - // Initialize chat visibility for all instances
4233 - $('.mxchat-chatbot-wrapper').each(function() {
4234 - var botId = $(this).data('bot-id') || 'default';
4235 - initializeChatVisibility(botId);
4236 - });
4237 -
4238 - // Make functions globally available for add-ons
4239 - window.hasQuickQuestions = hasQuickQuestions;
4240 - window.collapseQuickQuestions = collapseQuickQuestions;
4241 - window.appendMessage = appendMessage;
4242 - window.appendThinkingMessage = appendThinkingMessage;
4243 - window.scrollToBottom = scrollToBottom;
4244 - window.scrollElementToTop = scrollElementToTop;
4245 - window.replaceLastMessage = replaceLastMessage;
4246 - window.callMxChat = callMxChat;
4247 - window.callMxChatStream = callMxChatStream;
4248 - window.shouldUseStreaming = shouldUseStreaming;
4249 - window.getChatSession = getChatSession;
4250 - window.getPageContext = getPageContext;
4251 - window.updateStreamingMessage = updateStreamingMessage;
4252 - window.MxChatInstances = MxChatInstances;
4253 - window.getElement = getElement;
4254 - window.getElementDOM = getElementDOM;
4255 - window.getBotIdFromElement = getBotIdFromElement;
4256 -
4257 3073 }); // End of jQuery ready
4258 3074
4259 3075
4260 3076 // ====================================
@@ -4281,312 +3097,6 @@
4281 3097 }, 2000);
4282 3098 });
4283 3099 }
4284 3100 }
4285 -});
4286 -
4287 -// ============================================================================
4288 -// SATISFACTION RATING (v3.2.6)
4289 -// ============================================================================
4290 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4291 -// inactivity following a bot reply. One prompt per session, deduped via
4292 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4293 -// the option (default off) is authoritative.
4294 -jQuery(function($) {
4295 - if (typeof mxchatChat === 'undefined') return;
4296 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4297 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4298 - // old "disabled when exactly false/'off'" check let '' through and the
4299 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4300 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4301 - // (boolean-true localizations) working.
4302 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4303 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4304 - // merges fresh values over mxchatChat (that merge fires on first widget
4305 - // open, after this module has already decided). Re-evaluating would mean
4306 - // restructuring the whole module to late-bind its listeners — not worth it
4307 - // for a prompt that is at worst stale for one page load on a cached page.
4308 - var sre = mxchatChat.satisfaction_rating_enabled;
4309 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4310 -
4311 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4312 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4313 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4314 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4315 - if (idleSeconds < 5) idleSeconds = 5;
4316 - if (idleSeconds > 600) idleSeconds = 600;
4317 - var IDLE_MS = idleSeconds * 1000;
4318 - var MIN_BOT_REPLIES = 2;
4319 - var ratingState = {};
4320 -
4321 - function getState(botId) {
4322 - if (!ratingState[botId]) {
4323 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4324 - }
4325 - return ratingState[botId];
4326 - }
4327 -
4328 - function getSessionId(botId) {
4329 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4330 - return MxChatInstances.getChatSession(botId);
4331 - }
4332 - return null;
4333 - }
4334 -
4335 - function isAlreadyRated(sessionId) {
4336 - if (!sessionId) return false;
4337 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4338 - }
4339 -
4340 - function markRated(sessionId) {
4341 - if (!sessionId) return;
4342 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4343 - }
4344 -
4345 - function esc(s) {
4346 - return String(s == null ? '' : s)
4347 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4348 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4349 - }
4350 -
4351 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4352 - function ratingSkipInlineColors(botId) {
4353 - if (mxchatChat.skip_inline_colors) return true;
4354 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4355 - return botAssignments.hasOwnProperty(botId);
4356 - }
4357 -
4358 - function botBubbleStyleAttr(botId) {
4359 - if (ratingSkipInlineColors(botId)) return '';
4360 - var bg = mxchatChat.bot_message_bg_color;
4361 - var fg = mxchatChat.bot_message_font_color;
4362 - if (!bg && !fg) return '';
4363 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4364 - }
4365 -
4366 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4367 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4368 - // and paints the filled "Send" pill so it fills with the bot font color and
4369 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4370 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4371 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4372 - // unreliably inside a descendant's `background`, so a bubble-level var would
4373 - // silently fall back to the literal (white-block bug all over again). Inline
4374 - // longhand always wins. Same transparent-guard as the menu so we never paint
4375 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4376 - function syncRatingBubbleColors(botId) {
4377 - var $chatBox = getChatBoxByBotId(botId);
4378 - if (!$chatBox || !$chatBox.length) return;
4379 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4380 - if (!bubbleEl) return;
4381 - var cs = window.getComputedStyle(bubbleEl);
4382 - var fg = cs.color;
4383 - var bg = cs.backgroundColor;
4384 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4385 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4386 - // Expose on the bubble too, for any inheriting styles / future use.
4387 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4388 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4389 - // Paint the Send pill directly — the part that actually fixes the bug.
4390 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4391 - if (submitEl) {
4392 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4393 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4394 - }
4395 - }
4396 -
4397 - function copy(key) {
4398 - var c = mxchatChat.satisfaction_rating_copy || {};
4399 - var d = {
4400 - question: 'Was this helpful?',
4401 - helpful: 'Helpful',
4402 - not_helpful: 'Not helpful',
4403 - dismiss: 'Dismiss',
4404 - thanks: 'Thanks! Anything we should improve? (optional)',
4405 - placeholder: 'Tell us what could be better…',
4406 - send: 'Send',
4407 - skip: 'Skip',
4408 - saved: 'Thanks for the feedback.'
4409 - };
4410 - return c[key] || d[key];
4411 - }
4412 -
4413 - function thumbUpSvg() {
4414 - 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>';
4415 - }
4416 - function thumbDownSvg() {
4417 - 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>';
4418 - }
4419 -
4420 - function buildPromptHtml(botId) {
4421 - var styleAttr = botBubbleStyleAttr(botId);
4422 - return ''
4423 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4424 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4425 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4426 - + '<div class="mxchat-rating-actions">'
4427 - + '<span class="mxchat-rating-buttons">'
4428 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4429 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4430 - + '</span>'
4431 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4432 - + '</div>'
4433 - + '</div>'
4434 - + '</div>';
4435 - }
4436 -
4437 - function buildFeedbackHtml(botId, rating) {
4438 - var styleAttr = botBubbleStyleAttr(botId);
4439 - return ''
4440 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4441 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4442 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4443 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4444 - + '<div class="mxchat-rating-feedback-actions">'
4445 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4446 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4447 - + '</div>'
4448 - + '</div>'
4449 - + '</div>';
4450 - }
4451 -
4452 - function buildSavedHtml(botId) {
4453 - var styleAttr = botBubbleStyleAttr(botId);
4454 - return ''
4455 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4456 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4457 - + '</div>';
4458 - }
4459 -
4460 - function getChatBoxByBotId(botId) {
4461 - var $byId = $('#chat-box-' + botId);
4462 - if ($byId.length) return $byId.first();
4463 - return $('.chat-box').first();
4464 - }
4465 -
4466 - function scrollChatBoxToBottom($chatBox) {
4467 - if (!$chatBox || !$chatBox.length) return;
4468 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4469 - }
4470 -
4471 - function showPrompt(botId) {
4472 - var s = getState(botId);
4473 - if (s.promptShown || s.dismissed) return;
4474 - var sessionId = getSessionId(botId);
4475 - if (!sessionId) return;
4476 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4477 - var $chatBox = getChatBoxByBotId(botId);
4478 - if (!$chatBox.length) return;
4479 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4480 - $chatBox.append(buildPromptHtml(botId));
4481 - syncRatingBubbleColors(botId);
4482 - s.promptShown = true;
4483 - scrollChatBoxToBottom($chatBox);
4484 - }
4485 -
4486 - function submitRating(botId, rating, feedback) {
4487 - var sessionId = getSessionId(botId);
4488 - if (!sessionId) return;
4489 - $.post(mxchatChat.ajax_url, {
4490 - action: 'mxchat_save_rating',
4491 - session_id: sessionId,
4492 - bot_id: botId,
4493 - rating: rating,
4494 - feedback: feedback || ''
4495 - });
4496 - markRated(sessionId);
4497 - }
4498 -
4499 - function onBotReply(botId) {
4500 - var s = getState(botId);
4501 - s.botReplies += 1;
4502 - if (s.promptShown || s.dismissed) return;
4503 - var sessionId = getSessionId(botId);
4504 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4505 - if (s.botReplies < MIN_BOT_REPLIES) return;
4506 - if (s.idleTimer) clearTimeout(s.idleTimer);
4507 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4508 - }
4509 -
4510 - function onUserMessage(botId) {
4511 - var s = getState(botId);
4512 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4513 - }
4514 -
4515 - function botIdFromChatBox(el) {
4516 - var id = el && el.id ? el.id : '';
4517 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4518 - }
4519 -
4520 - function setupObserver(chatBox) {
4521 - var botId = botIdFromChatBox(chatBox);
4522 - try {
4523 - var observer = new MutationObserver(function(mutations) {
4524 - mutations.forEach(function(m) {
4525 - for (var i = 0; i < m.addedNodes.length; i++) {
4526 - var node = m.addedNodes[i];
4527 - if (!node || node.nodeType !== 1) continue;
4528 - var $n = $(node);
4529 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4530 - 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)
4531 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4532 - }
4533 - });
4534 - });
4535 - observer.observe(chatBox, { childList: true });
4536 - } catch (e) { /* noop */ }
4537 - }
4538 -
4539 - $('.chat-box').each(function() { setupObserver(this); });
4540 -
4541 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4542 - e.preventDefault();
4543 - var $btn = $(this);
4544 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4545 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4546 - var botId = $prompt.data('bot-id') || 'default';
4547 - var rating = parseInt($btn.attr('data-rating'), 10);
4548 - if (rating !== 1 && rating !== -1) return;
4549 - submitRating(botId, rating, '');
4550 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4551 - syncRatingBubbleColors(botId);
4552 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4553 - });
4554 -
4555 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4556 - e.preventDefault();
4557 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4558 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4559 - var botId = $prompt.data('bot-id') || 'default';
4560 - var s = getState(botId);
4561 - s.dismissed = true;
4562 - markRated(getSessionId(botId));
4563 - ($wrap.length ? $wrap : $prompt).remove();
4564 - });
4565 -
4566 - function closeFeedback($fb) {
4567 - var botId = $fb.data('bot-id') || 'default';
4568 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4569 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4570 - syncRatingBubbleColors(botId);
4571 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4572 - }
4573 -
4574 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4575 - e.preventDefault();
4576 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4577 - });
4578 -
4579 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4580 - e.preventDefault();
4581 - var $fb = $(this).closest('.mxchat-rating-feedback');
4582 - var botId = $fb.data('bot-id') || 'default';
4583 - var rating = parseInt($fb.attr('data-rating'), 10);
4584 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4585 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4586 - if (text !== '') {
4587 - submitRating(botId, rating, text);
4588 - }
4589 - closeFeedback($fb);
4590 - });
4591 3101 });
4592 3102