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

mxchat-admin.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.14, at js/mxchat-admin.js

3,754 lines 172.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // ─── Modal close safety ──────────────────────────────────────────────────
15 // A click event fires on the common ancestor of its mousedown and mouseup, so
16 // releasing a text-selection drag past a dialog's edge dispatches the click on
17 // the overlay. Modals holding editable fields therefore never close on the
18 // overlay and confirm before discarding; read-only ones require the whole
19 // gesture to land on the overlay.
20
21 // Serializes a modal's visible fields so edits can be detected on close.
22 function mxchatModalSnapshot(modal) {
23 const parts = [];
24 modal.querySelectorAll('input, textarea, select').forEach((field) => {
25 if (field.type === 'hidden') return;
26 parts.push(field.type === 'checkbox' || field.type === 'radio' ? (field.checked ? '1' : '0') : field.value);
27 });
28 return JSON.stringify(parts);
29 }
30
31 // Wraps a modal's closeModal so an explicit close confirms when fields changed.
32 function mxchatGuardedClose(modal, closeModal) {
33 const snapshot = mxchatModalSnapshot(modal);
34 const message = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.discard_changes_confirm)
35 ? mxchatAdmin.discard_changes_confirm
36 : 'Discard your unsaved changes?';
37 return (e) => {
38 if (mxchatModalSnapshot(modal) !== snapshot && !window.confirm(message)) return;
39 closeModal(e);
40 };
41 }
42
43 // Esc-to-close for a modal. Binds one named keydown handler and returns the detach
44 // function the modal's own close path must call. `{ once: true }` cannot be used
45 // here: it removes the listener on the first keydown of ANY key, so typing a single
46 // character killed Esc for the rest of the modal's life, and every open that saw no
47 // keypress left another listener stacked on document.
48 function mxchatBindEscClose(modal, onEscape) {
49 function onKeydown(e) {
50 if (e.key === 'Escape' && modal.classList.contains('active')) {
51 onEscape(e);
52 }
53 }
54 document.addEventListener('keydown', onKeydown);
55 return function detachEsc() {
56 document.removeEventListener('keydown', onKeydown);
57 };
58 }
59
60 // Overlay-close for read-only modals, ignoring clicks that began inside.
61 function mxchatDragSafeOverlayClose(modal, closeModal) {
62 let downOnOverlay = false;
63 modal.addEventListener('mousedown', (e) => { downOnOverlay = (e.target === modal); });
64 modal.addEventListener('click', (e) => {
65 const overlayGesture = downOnOverlay;
66 downOnOverlay = false;
67 if (e.target === modal && overlayGesture) closeModal(e);
68 });
69 }
70
71 // Helper function to open edit modal for intents/actions
72 function mxchatOpenEditModal(intentId, phrases) {
73 const modal = document.getElementById('mxchat-edit-modal');
74 if (!modal) return;
75
76 // Get form fields
77 const intentIdField = document.getElementById('edit_intent_id');
78 const phrasesField = document.getElementById('edit_phrases');
79
80 // Set values
81 intentIdField.value = intentId;
82 phrasesField.value = phrases;
83
84 // Show modal with animation
85 modal.style.display = 'flex';
86 requestAnimationFrame(() => {
87 modal.classList.add('active');
88 });
89
90 // Set up close handlers
91 let detachEsc = null;
92 const closeModal = () => {
93 if (detachEsc) {
94 detachEsc();
95 detachEsc = null;
96 }
97 modal.classList.remove('active');
98 setTimeout(() => {
99 modal.style.display = 'none';
100 }, 300); // Match the CSS transition time
101 };
102
103 // This modal holds edits: close only on explicit controls, confirming when dirty.
104 const guardedClose = mxchatGuardedClose(modal, closeModal);
105
106 // Close button handler
107 const closeBtn = modal.querySelector('.mxchat-modal-close');
108 if (closeBtn) {
109 closeBtn.onclick = guardedClose;
110 }
111
112 // Cancel button handler
113 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
114 if (cancelBtn) {
115 cancelBtn.onclick = guardedClose;
116 }
117
118 // Escape key to close modal
119 detachEsc = mxchatBindEscClose(modal, guardedClose);
120
121 // Focus the textarea
122 phrasesField.focus();
123 }
124 // Live Agent Notice Dismissal Function
125 function dismissLiveAgentNotice() {
126 if (typeof jQuery !== 'undefined' && typeof mxchatLiveAgent !== 'undefined') {
127 jQuery.post(mxchatLiveAgent.ajaxurl, {
128 action: 'dismiss_live_agent_notice',
129 nonce: mxchatLiveAgent.nonce
130 }, function(response) {
131 if (response.success) {
132 jQuery('#mxchat-disabled-notice').fadeOut(300);
133 }
134 }).fail(function() {
135 // Fallback: just hide the notice if AJAX fails
136 jQuery('#mxchat-disabled-notice').fadeOut(300);
137 });
138 } else {
139 // Fallback for cases where jQuery or localized data isn't available
140 var notice = document.getElementById('mxchat-disabled-notice');
141 if (notice) {
142 notice.style.display = 'none';
143 }
144 }
145 }
146
147 // Theme Migration Notice Dismissal Function
148 function dismissThemeMigrationNotice() {
149 if (typeof jQuery !== 'undefined' && typeof mxchatThemeMigration !== 'undefined') {
150 jQuery.post(mxchatThemeMigration.ajaxurl, {
151 action: 'dismiss_theme_migration_notice',
152 nonce: mxchatThemeMigration.nonce
153 }, function(response) {
154 if (response.success) {
155 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
156 }
157 }).fail(function() {
158 // Fallback: just hide the notice if AJAX fails
159 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
160 });
161 } else {
162 // Fallback for cases where jQuery or localized data isn't available
163 var notice = document.getElementById('mxchat-theme-migration-notice');
164 if (notice) {
165 notice.style.display = 'none';
166 }
167 }
168 }
169
170 // Updated mxchatOpenActionModal function to integrate with the new selector
171 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
172 const modal = document.getElementById('mxchat-action-modal');
173 if (!modal) return;
174
175 // Get form fields
176 const actionIdField = document.getElementById('edit_action_id');
177 const labelField = document.getElementById('intent_label');
178 const phrasesField = document.getElementById('action_phrases');
179 const formActionType = document.getElementById('form_action_type');
180 const callbackGroup = document.getElementById('callback_selection_group');
181 const callbackSelect = document.getElementById('callback_function');
182 const saveButton = document.getElementById('mxchat-save-action-btn');
183 const nonceContainer = document.getElementById('action-nonce-container');
184 const thresholdSlider = document.getElementById('similarity_threshold');
185 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
186
187 // Set up modal for edit or create
188 if (isEdit) {
189 saveButton.textContent = 'Update Action';
190 formActionType.value = 'mxchat_edit_intent';
191 actionIdField.value = actionId;
192 labelField.value = label;
193 phrasesField.value = phrases;
194 callbackGroup.style.display = 'none'; // Hide callback selection when editing
195 thresholdSlider.value = threshold; // Set the current threshold value
196 thresholdDisplay.textContent = threshold + '%'; // Update display
197
198 // Remove the required attribute when editing
199 callbackSelect.removeAttribute('required');
200
201 // Update the nonce field for editing
202 nonceContainer.innerHTML = ''; // Clear existing nonce
203 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
204 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
205 }
206 } else {
207 saveButton.textContent = 'Save Action';
208 formActionType.value = 'mxchat_add_intent';
209 actionIdField.value = '';
210 labelField.value = '';
211 phrasesField.value = '';
212 callbackGroup.style.display = 'block'; // Show callback selection when creating
213 thresholdSlider.value = 85; // Default value for new actions
214 thresholdDisplay.textContent = '85%'; // Default display
215
216 // Ensure the required attribute is present when adding
217 callbackSelect.setAttribute('required', 'required');
218
219 // Update the nonce field for adding
220 nonceContainer.innerHTML = ''; // Clear existing nonce
221 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
222 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
223 }
224 }
225
226 // Show modal with animation
227 modal.style.display = 'flex';
228 requestAnimationFrame(() => {
229 modal.classList.add('active');
230 });
231
232 // Set up close handlers
233 let detachEsc = null;
234 const closeModal = () => {
235 if (detachEsc) {
236 detachEsc();
237 detachEsc = null;
238 }
239 modal.classList.remove('active');
240 setTimeout(() => {
241 modal.style.display = 'none';
242 }, 300); // Match the CSS transition time
243 };
244
245 // This modal holds edits: close only on explicit controls, confirming when dirty.
246 const guardedClose = mxchatGuardedClose(modal, closeModal);
247
248 // Close button handler
249 const closeBtn = modal.querySelector('.mxchat-modal-close');
250 if (closeBtn) {
251 closeBtn.onclick = guardedClose;
252 }
253
254 // Cancel button handler
255 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
256 if (cancelBtn) {
257 cancelBtn.onclick = guardedClose;
258 }
259
260 // Escape key to close modal
261 detachEsc = mxchatBindEscClose(modal, guardedClose);
262
263 // Focus the first field
264 labelField.focus();
265
266 // Dispatch an event for the action type selector to catch
267 const event = new CustomEvent('mxchatModalOpened', {
268 detail: {
269 isEdit: isEdit,
270 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
271 }
272 });
273 document.dispatchEvent(event);
274
275 return closeModal; // Return close function for external use
276 }
277
278 // Initialize event listeners
279 document.addEventListener('DOMContentLoaded', () => {
280 // Set up edit button handlers for intents
281 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
282 button.onclick = () => {
283 const intentId = button.dataset.intentId;
284 const phrases = button.dataset.phrases;
285 mxchatOpenEditModal(intentId, phrases);
286 };
287 });
288
289 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
290 button.onclick = () => {
291 const actionId = button.dataset.actionId;
292 const phrases = button.dataset.phrases;
293 const label = button.dataset.label;
294 const threshold = button.dataset.threshold || 85;
295 const callbackFunction = button.dataset.callbackFunction;
296 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
297
298 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
299 };
300 });
301
302 // Set up add new action buttons (new functionality)
303 const addActionBtn = document.getElementById('mxchat-add-action-btn');
304 if (addActionBtn) {
305 addActionBtn.onclick = () => mxchatOpenActionModal();
306 }
307
308 const createFirstAction = document.getElementById('mxchat-create-first-action');
309 if (createFirstAction) {
310 createFirstAction.onclick = () => mxchatOpenActionModal();
311 }
312
313 // Setup category-specific new action buttons (new functionality)
314 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
315 button.onclick = () => {
316 const category = button.closest('.mxchat-new-action-card').dataset.category;
317 const closeModal = mxchatOpenActionModal();
318
319 // Pre-select the appropriate callback based on category
320 if (category) {
321 const callbackSelect = document.getElementById('callback_function');
322 if (callbackSelect) {
323 setTimeout(() => {
324 // Map categories to default callbacks
325 const categoryToCallback = {
326 'data_collection': 'mxchat_handle_form_collection',
327 'integrations': 'mxchat_handle_slack_message',
328 'custom_actions': 'mxchat_handle_custom_action',
329 'recommendations': 'mxchat_handle_product_recommendations'
330 // Add more mappings as needed
331 };
332
333 if (categoryToCallback[category]) {
334 callbackSelect.value = categoryToCallback[category];
335 }
336 }, 100);
337 }
338 }
339 };
340 });
341
342 // Handle action toggle switches (new functionality)
343 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
344 toggle.onchange = function() {
345 const actionId = this.dataset.actionId;
346 const isEnabled = this.checked;
347
348 // Show loading indicator
349 const loadingEl = document.getElementById('mxchat-action-loading');
350 if (loadingEl) loadingEl.style.display = 'flex';
351
352 // Send AJAX request to update status
353 fetch(ajaxurl, {
354 method: 'POST',
355 headers: {
356 'Content-Type': 'application/x-www-form-urlencoded',
357 },
358 body: new URLSearchParams({
359 action: 'mxchat_toggle_action',
360 intent_id: actionId,
361 enabled: isEnabled ? 1 : 0,
362 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
363 })
364 })
365 .then(response => response.json())
366 .then(data => {
367 if (!data.success) {
368 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
369 this.checked = !isEnabled; // Revert the toggle
370 }
371 })
372 .catch(error => {
373 //console.error('Error:', error);
374 alert('Server error. Please try again.');
375 this.checked = !isEnabled; // Revert the toggle
376 })
377 .finally(() => {
378 if (loadingEl) loadingEl.style.display = 'none';
379 });
380 };
381 });
382
383 // Handle threshold sliders in action cards (new functionality)
384 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
385 slider.oninput = function() {
386 const actionId = this.id.replace('intent_threshold_', '');
387 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
388 };
389 });
390
391 // Handle threshold save buttons in action cards (new functionality)
392 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
393 button.onclick = function(e) {
394 e.preventDefault();
395 const form = this.closest('form');
396 const intentId = form.querySelector('input[name="intent_id"]').value;
397 const threshold = form.querySelector('input[name="intent_threshold"]').value;
398 const nonce = form.querySelector('input[name="_wpnonce"]').value;
399
400 // Show loading indicator
401 const loadingEl = document.getElementById('mxchat-action-loading');
402 if (loadingEl) loadingEl.style.display = 'flex';
403
404 // Send AJAX request
405 fetch(ajaxurl, {
406 method: 'POST',
407 headers: {
408 'Content-Type': 'application/x-www-form-urlencoded',
409 },
410 body: new URLSearchParams({
411 action: 'mxchat_update_intent_threshold',
412 intent_id: intentId,
413 intent_threshold: threshold,
414 _wpnonce: nonce
415 })
416 })
417 .then(response => response.json())
418 .then(data => {
419 if (data.success) {
420 // Visual feedback of success
421 const card = this.closest('.mxchat-action-card');
422 card.style.background = 'rgba(120, 115, 245, 0.1)';
423 setTimeout(() => {
424 card.style.background = 'white';
425 }, 300);
426 } else {
427 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
428 }
429 })
430 .catch(error => {
431 //console.error('Error:', error);
432 alert('Server error. Please try again.');
433 })
434 .finally(() => {
435 if (loadingEl) loadingEl.style.display = 'none';
436 });
437 };
438 });
439 });
440
441 jQuery(document).ready(function($) {
442 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
443 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
444
445 // --- AJAX Auto-Save ---
446 let $autosaveSections = $('.mxchat-autosave-section');
447
448 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
449 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
450 if ($pineconeAutosaveSection.length) {
451 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
452 //console.log('Added Pinecone section to auto-save monitoring');
453 }
454
455 // Track whether fields have been modified by user
456 const userModifiedFields = new Set();
457
458 if ($autosaveSections.length) {
459 // Track user interactions with input fields to determine if changes are user-initiated
460 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
461 const fieldName = $(this).attr('name');
462 if (fieldName) {
463 userModifiedFields.add(fieldName);
464 }
465 });
466
467 // Handle real-time range slider value updates
468 $autosaveSections.find('input[type="range"]').on('input', function() {
469 const value = $(this).val();
470 const $slider = $(this);
471 const sliderId = $slider.attr('id');
472 // Find the corresponding value display span (convention: id_value)
473 const $valueSpan = $('#' + sliderId + '_value');
474 if ($valueSpan.length) {
475 $valueSpan.text(value);
476 } else {
477 // Fallback for similarity_threshold which uses threshold_value
478 $('#threshold_value').text(value);
479 }
480 });
481
482 // Handle all input changes (including range slider)
483 // Store pending AJAX requests and debounce timers per field to prevent freezing
484 const pendingRequests = {};
485 const fieldDebounceTimers = {};
486
487 // Helper function to save rate limit fields with request tracking
488 function triggerFieldSave($field, name, pendingRequests) {
489 const value = $field.val();
490
491 // Remove any existing feedback containers for this field
492 $field.siblings('.feedback-container').remove();
493 $field.parent().find('.feedback-container').remove();
494
495 // Create feedback container
496 const feedbackContainer = $('<div class="feedback-container"></div>');
497 const spinner = $('<div class="saving-spinner"></div>');
498 const successIcon = $('<div class="success-icon">✔</div>');
499 $field.after(feedbackContainer);
500 feedbackContainer.append(spinner);
501
502 // Rate limits use the main settings action
503 const ajaxAction = 'mxchat_save_setting';
504 const nonce = mxchatAdmin.setting_nonce;
505
506 // Store the AJAX request so it can be aborted if needed
507 pendingRequests[name] = $.ajax({
508 url: mxchatAdmin.ajax_url,
509 type: 'POST',
510 data: {
511 action: ajaxAction,
512 name: name,
513 value: value,
514 _ajax_nonce: nonce
515 },
516 success: function(response) {
517 delete pendingRequests[name];
518 if (response.success) {
519 spinner.fadeOut(200, function() {
520 feedbackContainer.append(successIcon);
521 successIcon.fadeIn(200).delay(800).fadeOut(200, function() {
522 feedbackContainer.remove();
523 });
524 });
525 } else {
526 feedbackContainer.remove();
527 }
528 },
529 error: function(xhr, textStatus, error) {
530 delete pendingRequests[name];
531 // Don't show error for aborted requests
532 if (textStatus !== 'abort') {
533 feedbackContainer.remove();
534 }
535 }
536 });
537 }
538
539 // .mxchat-la-field — the live-agent schedule editors' day inputs (plans
540 // 8ccaa2 + 99d7a4, one editor per channel). They are nameless by design:
541 // each editor folds them into its own hidden live_agent_schedule_<channel>
542 // input and fires ONE change, which this same handler then saves. Without
543 // the exclusion each keystroke would POST a nameless field the server can
544 // only reject.
545 $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model, .mxchat-la-field').on('change', function() {
546 const $field = $(this);
547 const name = $field.attr('name');
548
549 // Debounce rate limit fields to prevent UI freezing from rapid changes
550 if (name && name.indexOf('rate_limits') !== -1) {
551 // Clear any pending timer for this field
552 if (fieldDebounceTimers[name]) {
553 clearTimeout(fieldDebounceTimers[name]);
554 }
555 // Abort any pending AJAX request for this field
556 if (pendingRequests[name]) {
557 pendingRequests[name].abort();
558 // Remove any existing feedback containers for this field
559 $field.siblings('.feedback-container').remove();
560 $field.parent().find('.feedback-container').remove();
561 }
562 // Debounce the save operation
563 const fieldRef = $field;
564 fieldDebounceTimers[name] = setTimeout(function() {
565 triggerFieldSave(fieldRef, name, pendingRequests);
566 }, 300);
567 return;
568 }
569
570 // Skip saving for API key fields that haven't been interacted with and are empty
571 const isApiKeyField = name && (
572 name === 'loops_api_key' ||
573 name === 'api_key' ||
574 name === 'xai_api_key' ||
575 name === 'claude_api_key' ||
576 name === 'voyage_api_key' ||
577 name === 'gemini_api_key' ||
578 name === 'deepseek_api_key' ||
579 name.indexOf('_api_key') !== -1
580 );
581
582 // Skip processing if:
583 // 1. It's an API key field
584 // 2. The user hasn't interacted with it
585 // 3. The field is empty
586 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
587 //console.log('Skipping auto-save for untouched API key field:', name);
588 return;
589 }
590
591 let value;
592
593 // Handle different input types
594 if ($field.attr('type') === 'checkbox') {
595 // *** UPDATED: Handle Pinecone checkboxes differently ***
596 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
597 value = $field.is(':checked') ? '1' : '0';
598 } else {
599 value = $field.is(':checked') ? 'on' : 'off';
600 }
601 } else {
602 value = $field.val();
603 }
604
605 // Create feedback container
606 const feedbackContainer = $('<div class="feedback-container"></div>');
607 const spinner = $('<div class="saving-spinner"></div>');
608 const successIcon = $('<div class="success-icon">✔</div>');
609
610 // Position feedback container based on input type
611 if ($field.closest('.toggle-switch').length) {
612 // Try td first (old layout), then mxc-field-control (new card layout), then fallback to after toggle
613 var $container = $field.closest('td');
614 if (!$container.length) {
615 $container = $field.closest('.mxc-field-control');
616 }
617 if ($container.length) {
618 $container.append(feedbackContainer);
619 } else {
620 $field.closest('.toggle-switch').after(feedbackContainer);
621 }
622 } else if ($field.closest('.mxchat-toggle-switch').length) {
623 // Try mxchat-toggle-container first, then parent div, then fallback to after toggle
624 var $toggleContainer = $field.closest('.mxchat-toggle-container');
625 if ($toggleContainer.length) {
626 $toggleContainer.append(feedbackContainer);
627 } else {
628 $field.closest('.mxchat-toggle-switch').after(feedbackContainer);
629 }
630 } else if ($field.closest('.slider-container').length) {
631 $field.closest('.slider-container').after(feedbackContainer);
632 } else {
633 $field.after(feedbackContainer);
634 }
635 feedbackContainer.append(spinner);
636
637 // Determine which AJAX action and nonce to use:
638 var ajaxAction, nonce;
639 // *** UPDATED: Add Pinecone fields, chunking fields, ACF fields, and custom meta to prompts action ***
640 if (name.indexOf('mxchat_prompts_options') !== -1 ||
641 name === 'mxchat_auto_sync_posts' ||
642 name === 'mxchat_auto_sync_pages' ||
643 name.indexOf('mxchat_auto_sync_') === 0 ||
644 name.indexOf('mxchat_pinecone_addon_options') !== -1 ||
645 name.indexOf('mxchat_chunk') === 0 ||
646 name.indexOf('mxchat_acf_field_') === 0 ||
647 name === 'mxchat_custom_meta_whitelist') { // Chunking, ACF field settings, and custom meta
648 ajaxAction = 'mxchat_save_prompts_setting';
649 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
650 } else {
651 // Otherwise, use the existing AJAX action.
652 ajaxAction = 'mxchat_save_setting';
653 nonce = mxchatAdmin.setting_nonce;
654 }
655
656 // *** ADD THIS: Debug logging for Pinecone fields ***
657 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
658 //console.log('Saving Pinecone field:', name, '=', value);
659 }
660
661 // AJAX save request
662 $.ajax({
663 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
664 type: 'POST',
665 data: {
666 action: ajaxAction,
667 name: name,
668 value: value,
669 _ajax_nonce: nonce
670 },
671 success: function(response) {
672 if (response.success) {
673 spinner.fadeOut(200, function() {
674 feedbackContainer.append(successIcon);
675 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
676 feedbackContainer.remove();
677 });
678 });
679
680 // *** ADD THIS: Refresh API key status after saving an API key ***
681 const isApiKeyField = name && (
682 name === 'api_key' ||
683 name === 'xai_api_key' ||
684 name === 'claude_api_key' ||
685 name === 'voyage_api_key' ||
686 name === 'gemini_api_key' ||
687 name === 'deepseek_api_key' ||
688 name === 'openrouter_api_key' ||
689 name.indexOf('_api_key') !== -1
690 );
691
692 if (isApiKeyField && typeof window.mxchatRefreshAPIKeyStatus === 'function') {
693 window.mxchatRefreshAPIKeyStatus();
694 }
695
696 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
697 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
698 //console.log('Pinecone toggle saved successfully, value:', value);
699
700 // The checkbox state is already updated by the user interaction
701 // But let's make sure the UI state matches the saved value
702 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
703 var settingsDiv = $('.mxchat-pinecone-settings');
704
705 // Double-check the UI state matches what was saved
706 if (value === '1' && !$checkbox.is(':checked')) {
707 $checkbox.prop('checked', true);
708 settingsDiv.slideDown(300);
709 } else if (value === '0' && $checkbox.is(':checked')) {
710 $checkbox.prop('checked', false);
711 settingsDiv.slideUp(300);
712 }
713
714 //console.log('Pinecone UI state synchronized');
715
716 // Check if Knowledge Import tab is currently active
717 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
718 // Show a notice that we need to refresh
719 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
720 if ($knowledgeCard.length > 0) {
721 // Add a refresh notice at the top of the knowledge base card
722 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
723 '<p style="margin: 0;">' +
724 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
725 'Database settings have changed. ' +
726 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
727 '</p></div>');
728
729 $knowledgeCard.prepend(refreshNotice);
730 }
731 } else {
732 // If not on import tab, set a flag to refresh when they go there
733 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
734 }
735 }
736
737 // *** ADD THIS: Debug logging for successful saves ***
738 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
739 //console.log('Pinecone field saved successfully:', name, '=', value);
740 }
741
742 // Check if the response contains a "no changes" message and log it
743 if (response.data && response.data.message === 'No changes detected') {
744 //console.log('No changes detected for field:', name);
745 }
746 } else {
747 // Only show alert for actual errors, not for "no changes"
748 let errorMessage = response.data?.message || 'Unknown error';
749
750 // Don't display an alert for "no changes" message
751 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
752 alert('Error saving: ' + errorMessage);
753 } else {
754 // Still provide visual feedback that no changes were needed
755 spinner.fadeOut(200, function() {
756 feedbackContainer.append(successIcon);
757 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
758 feedbackContainer.remove();
759 });
760 });
761 //console.log('No changes detected for field:', name);
762 return;
763 }
764
765 // Only revert checkbox state if it was an actual error
766 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
767 if ($field.attr('type') === 'checkbox') {
768 $field.prop('checked', !$field.is(':checked'));
769 }
770 }
771
772 // Always clean up the feedback container
773 feedbackContainer.remove();
774 }
775 },
776 error: function(xhr, textStatus, error) {
777 //console.error('AJAX Error:', textStatus, error);
778 alert('An error occurred while saving. Please try again.');
779
780 // Revert checkbox state on error
781 if ($field.attr('type') === 'checkbox') {
782 $field.prop('checked', !$field.is(':checked'));
783 }
784
785 feedbackContainer.remove();
786 }
787 });
788 });
789
790 // Initialize color pickers with debouncing
791 $autosaveSections.find('.my-color-field').each(function() {
792 const $colorField = $(this);
793
794 $(this).wpColorPicker({
795 change: useDebounce(function(event, ui) {
796 // Safety check - ensure we have a valid field and value
797 if (!$colorField || !$colorField.val()) {
798 //console.warn('Color picker not ready');
799 return;
800 }
801
802 const name = $colorField.attr('name');
803 const value = $colorField.val();
804
805 if (!name || !value) {
806 //console.warn('Missing required color picker values');
807 return;
808 }
809
810 // Create feedback container
811 const feedbackContainer = $('<div class="feedback-container"></div>');
812 const spinner = $('<div class="saving-spinner"></div>');
813 const successIcon = $('<div class="success-icon">✔</div>');
814
815 // Position feedback container
816 $colorField.closest('.wp-picker-container').after(feedbackContainer);
817 feedbackContainer.append(spinner);
818
819 // Determine which AJAX action and nonce to use:
820 var ajaxAction, nonce;
821 // Use the new AJAX action for submenu fields:
822 if (name.indexOf('mxchat_prompts_options') !== -1 ||
823 name === 'mxchat_auto_sync_posts' ||
824 name === 'mxchat_auto_sync_pages' ||
825 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
826 ajaxAction = 'mxchat_save_prompts_setting';
827 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
828 } else {
829 // Otherwise, use the existing AJAX action.
830 ajaxAction = 'mxchat_save_setting';
831 nonce = mxchatAdmin.setting_nonce;
832 }
833 // AJAX save request
834 $.ajax({
835 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
836 type: 'POST',
837 data: {
838 action: ajaxAction,
839 name: name,
840 value: value,
841 _ajax_nonce: nonce
842 },
843 success: function(response) {
844 if (response.success) {
845 spinner.fadeOut(200, function() {
846 feedbackContainer.append(successIcon);
847 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
848 feedbackContainer.remove();
849 });
850 });
851 } else {
852 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
853 feedbackContainer.remove();
854 }
855 },
856 error: function() {
857 alert('An error occurred while saving.');
858 feedbackContainer.remove();
859 }
860 });
861 }, 500)
862 });
863 });
864
865 }
866
867 // ========================================
868 // POST TYPE VISIBILITY SETTINGS
869 // ========================================
870 (function() {
871 const $appendToBody = $('#append_to_body');
872 const $visibilityOptions = $('#post-type-visibility-options');
873 const $modeRadios = $('input[name="post_type_visibility_mode"]');
874 const $postTypeList = $('#post-type-list');
875 const $postTypeCheckboxes = $('input[name="post_type_visibility_list[]"]');
876
877 // Toggle visibility options based on auto-display toggle
878 $appendToBody.on('change', function() {
879 if ($(this).is(':checked')) {
880 $visibilityOptions.slideDown(200);
881 } else {
882 $visibilityOptions.slideUp(200);
883 }
884 });
885
886 // Toggle post type list based on mode selection
887 $modeRadios.on('change', function() {
888 const mode = $(this).val();
889 if (mode === 'all') {
890 $postTypeList.slideUp(200);
891 } else {
892 $postTypeList.slideDown(200);
893 }
894
895 // Save mode via AJAX
896 savePostTypeVisibility('post_type_visibility_mode', mode);
897 });
898
899 // Save post type list when checkboxes change
900 $postTypeCheckboxes.on('change', function() {
901 // Collect all checked post types
902 const selectedPostTypes = [];
903 $postTypeCheckboxes.filter(':checked').each(function() {
904 selectedPostTypes.push($(this).val());
905 });
906
907 // Save as JSON array
908 savePostTypeVisibility('post_type_visibility_list', JSON.stringify(selectedPostTypes));
909 });
910
911 // Helper function to save post type visibility settings
912 function savePostTypeVisibility(name, value) {
913 if (typeof mxchatAdmin === 'undefined') return;
914
915 // Find the container element for feedback
916 const $container = name === 'post_type_visibility_mode'
917 ? $('.mxchat-visibility-mode')
918 : $postTypeList;
919
920 // Remove any existing feedback
921 $container.find('.mxchat-save-feedback').remove();
922
923 // Create feedback element
924 const $feedback = $('<span class="mxchat-save-feedback" style="margin-left: 10px; font-size: 12px;"></span>');
925 $feedback.text('Saving...').css('color', '#666');
926
927 if (name === 'post_type_visibility_mode') {
928 $container.append($feedback);
929 } else {
930 $container.before($feedback);
931 }
932
933 $.ajax({
934 url: mxchatAdmin.ajax_url,
935 type: 'POST',
936 data: {
937 action: 'mxchat_save_setting',
938 name: name,
939 value: value,
940 _ajax_nonce: mxchatAdmin.setting_nonce
941 },
942 success: function(response) {
943 if (response.success) {
944 $feedback.text('✓ Saved').css('color', '#46b450');
945 setTimeout(function() {
946 $feedback.fadeOut(300, function() {
947 $(this).remove();
948 });
949 }, 1500);
950 } else {
951 $feedback.text('Error saving').css('color', '#dc3232');
952 }
953 },
954 error: function() {
955 $feedback.text('Error saving').css('color', '#dc3232');
956 }
957 });
958 }
959 })();
960
961 // Toggle visibility handlers
962 function toggleVisibility(selector) {
963 $(selector).on('click', function() {
964 var inputField = $(this).prev('input');
965 // Check if this is a CSS-masked field (type="text" with mxchat-api-key-field class)
966 if (inputField.hasClass('mxchat-api-key-field')) {
967 // Use CSS class toggle for CSS-masked fields
968 if (inputField.hasClass('mxchat-show-key')) {
969 inputField.removeClass('mxchat-show-key');
970 $(this).text('Show');
971 } else {
972 inputField.addClass('mxchat-show-key');
973 $(this).text('Hide');
974 }
975 } else {
976 // Legacy type toggle for password fields
977 if (inputField.attr('type') === 'password') {
978 inputField.attr('type', 'text');
979 $(this).text('Hide');
980 } else {
981 inputField.attr('type', 'password');
982 $(this).text('Show');
983 }
984 }
985 });
986 }
987
988 // Initialize all toggle visibility buttons
989 [
990 '#toggleApiKeyVisibility',
991 '#toggleWooCommerceSecretVisibility',
992 '#toggleVoyageAPIKeyVisibility',
993 '#toggleLoopsApiKeyVisibility',
994 '#toggleXaiApiKeyVisibility',
995 '#toggleClaudeApiKeyVisibility',
996 '#toggleBraveApiKeyVisibility',
997 '#toggleWebhookUrlVisibility',
998 '#toggleSecretKeyVisibility',
999 '#toggleBotTokenVisibility',
1000 '#toggleDeepSeekApiKeyVisibility',
1001 '#toggleGeminiApiKeyVisibility',
1002 '#toggleOpenRouterApiKeyVisibility'
1003 ].forEach(toggleVisibility);
1004
1005 function setupMxChatModelSelector() {
1006 const $modelSelect = $('#model');
1007 const $modelSelectorButton = $('<button>', {
1008 type: 'button',
1009 id: 'mxchat_model_selector_btn',
1010 class: 'button-primary mxchat-model-selector-btn',
1011 text: 'Select AI Model'
1012 });
1013
1014 // Replace the select dropdown with a button
1015 $modelSelect.hide().after($modelSelectorButton);
1016
1017 // Update button text to show currently selected model
1018 function updateButtonText() {
1019 const selectedModel = $modelSelect.val();
1020
1021 // Check if OpenRouter is selected
1022 if (selectedModel === 'openrouter') {
1023 const openrouterModelId = $('#openrouter_selected_model').val();
1024 const openrouterModelName = $('#openrouter_selected_model_name').val();
1025
1026 if (openrouterModelName && openrouterModelName.trim() !== '') {
1027 $modelSelectorButton.text('OpenRouter: ' + openrouterModelName);
1028 } else if (openrouterModelId && openrouterModelId.trim() !== '') {
1029 $modelSelectorButton.text('OpenRouter: ' + openrouterModelId);
1030 } else {
1031 $modelSelectorButton.text('OpenRouter - Select Model');
1032 }
1033 } else {
1034 const selectedModelText = $modelSelect.find('option:selected').text();
1035 $modelSelectorButton.text(selectedModelText);
1036 }
1037 }
1038
1039 // Initialize button text
1040 updateButtonText();
1041
1042 // Create and append modal HTML
1043 const modelSelectorModal = `
1044 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
1045 <div class="mxchat-model-selector-modal-content">
1046 <div class="mxchat-model-selector-modal-header">
1047 <h3>Select AI Model</h3>
1048 <span class="mxchat-model-selector-modal-close">&times;</span>
1049 </div>
1050 <div class="mxchat-model-selector-modal-body">
1051 <div class="mxchat-model-selector-search-container">
1052 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1053 </div>
1054 <div class="mxchat-model-selector-categories">
1055 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1056 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1057 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1058 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1059 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1060 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1061 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1062 <button class="mxchat-model-category-btn" data-category="custom">Custom / Local</button>
1063 </div>
1064 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1065 </div>
1066 <div class="mxchat-model-selector-modal-footer">
1067 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1068 </div>
1069 </div>
1070 </div>
1071 `;
1072
1073 $('body').append(modelSelectorModal);
1074
1075 // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
1076 window.populateModelsGrid = function(filter = '', category = 'all') {
1077 const $grid = $('#mxchat_models_grid');
1078 $grid.empty();
1079
1080 // Catalog refactor (plan-d14e89): when class-mxchat-model-catalog.php
1081 // is loaded (via wp_localize_script as mxchatChatModelCatalog), use
1082 // its data so a single edit there flows to this picker grid. The
1083 // inline fallback below keeps the picker working if for some reason
1084 // the localize hasn't run (e.g. legacy admin page bootstrap order).
1085 const models = (typeof mxchatChatModelCatalog === 'object' && mxchatChatModelCatalog) ? mxchatChatModelCatalog : {
1086 openrouter: [
1087 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
1088 ],
1089 gemini: [
1090 { value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', description: 'Stable — newest Flash generation, recommended default' },
1091 ],
1092 openai: [
1093 { value: 'gpt-5.1-chat-latest', label: 'GPT-5.1 Chat Latest', description: 'Recommended for most use cases' },
1094 ],
1095 claude: [
1096 { value: 'claude-fable-5', label: 'Claude Fable 5', description: 'Latest Flagship — newest and most capable Anthropic model' },
1097 { value: 'claude-opus-4-8', label: 'Claude Opus 4.8', description: 'Previous flagship — most capable Opus-tier model' },
1098 { value: 'claude-opus-4-7', label: 'Claude Opus 4.7', description: 'Previous Anthropic flagship model' },
1099 ],
1100 xai: [
1101 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model' },
1102 ],
1103 deepseek: [
1104 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
1105 ],
1106 custom: [
1107 { value: 'custom-provider', label: 'Custom Provider', description: 'OpenAI-compatible local LLM — configure in API Keys tab' },
1108 ],
1109 };
1110
1111 let allModels = [];
1112 Object.keys(models).forEach(key => {
1113 if (category === 'all' || category === key) {
1114 allModels = allModels.concat(models[key]);
1115 }
1116 });
1117
1118 // Filter by search term if present
1119 if (filter) {
1120 const lowerFilter = filter.toLowerCase();
1121 allModels = allModels.filter(model =>
1122 model.label.toLowerCase().includes(lowerFilter) ||
1123 model.description.toLowerCase().includes(lowerFilter)
1124 );
1125 }
1126
1127 // Create model cards
1128 allModels.forEach(model => {
1129 const isSelected = $modelSelect.val() === model.value;
1130 const $modelCard = $(`
1131 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
1132 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
1133 <div class="mxchat-model-selector-info">
1134 <h4 class="mxchat-model-selector-title">${model.label}</h4>
1135 <p class="mxchat-model-selector-description">${model.description}</p>
1136 </div>
1137 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1138 </div>
1139 `);
1140 $grid.append($modelCard);
1141 });
1142 };
1143
1144 // Helper function to get icon for each model
1145 function getModelIcon(modelValue) {
1146 if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
1147 if (modelValue.startsWith('gemini-')) return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" class="mxchat-model-icon-gemini"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"></path></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"></use></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"></path><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"></path><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"></path><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"></path></svg>';
1148 if (modelValue.startsWith('gpt-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
1149 if (modelValue.startsWith('claude-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 176" fill="none" class="mxchat-model-icon-claude"><path fill="currentColor" d="m147.487 0l70.081 175.78H256L185.919 0zM66.183 106.221l23.98-61.774l23.98 61.774zM70.07 0L0 175.78h39.18l14.33-36.914h73.308l14.328 36.914h39.179L110.255 0z"></path></svg>';
1150 if (modelValue.startsWith('grok-')) return '<svg fill="currentColor" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mxchat-model-icon-xai"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>';
1151 if (modelValue === 'custom-provider' || modelValue.startsWith('custom-')) return '<span class="dashicons dashicons-admin-site-alt3" style="font-size: 24px; color: #2c8a3d;"></span>';
1152 if (modelValue.startsWith('deepseek-')) return '<svg height="1em" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg" class="mxchat-model-icon-deepseek"><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="currentColor"></path></svg>';
1153 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1154 }
1155
1156 // Event handlers
1157 $modelSelectorButton.on('click', function() {
1158 $('#mxchat_model_selector_modal').show();
1159 window.populateModelsGrid('', 'all');
1160 });
1161
1162 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1163 $('#mxchat_model_selector_modal').hide();
1164 });
1165
1166 $('.mxchat-model-category-btn').on('click', function() {
1167 $('.mxchat-model-category-btn').removeClass('active');
1168 $(this).addClass('active');
1169 const category = $(this).data('category');
1170 const searchTerm = $('#mxchat_model_search_input').val();
1171 window.populateModelsGrid(searchTerm, category);
1172 });
1173
1174 $('#mxchat_model_search_input').on('input', function() {
1175 const searchTerm = $(this).val();
1176 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1177 window.populateModelsGrid(searchTerm, activeCategory);
1178 });
1179
1180 $(document).on('click', '.mxchat-model-selector-card', function() {
1181 const modelValue = $(this).data('value');
1182 const $modelSelect = $('#model');
1183 const $clickedCard = $(this);
1184
1185 // Check if OpenRouter was selected
1186 if (modelValue === 'openrouter') {
1187 // Load OpenRouter models instead of closing
1188 loadOpenRouterModels();
1189 } else {
1190 // Remove selection from all other cards
1191 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1192
1193 // Add selection to clicked card immediately for instant feedback
1194 $clickedCard.addClass('mxchat-model-selected');
1195 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1196 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1197 }
1198
1199 // Brief delay to show the selection, then start saving
1200 setTimeout(function() {
1201 // Normal model selection
1202 $modelSelect.val(modelValue).trigger('change');
1203
1204 // Show loading state on the card
1205 $clickedCard.css('pointer-events', 'none');
1206 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1207 $clickedCard.find('.mxchat-model-selector-title').html(
1208 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1209 );
1210
1211 // Manually save the model via AJAX
1212 jQuery.ajax({
1213 url: mxchatAdmin.ajax_url,
1214 type: 'POST',
1215 data: {
1216 action: 'mxchat_save_setting',
1217 name: 'model',
1218 value: modelValue,
1219 _ajax_nonce: mxchatAdmin.setting_nonce
1220 },
1221 success: function(response) {
1222 if (response.success) {
1223 // Show success state
1224 $clickedCard.find('.mxchat-model-selector-title').html(
1225 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1226 );
1227
1228 // Update button text after successful save
1229 const selectedModelText = $modelSelect.find('option:selected').text();
1230 $('#mxchat_model_selector_btn').text(selectedModelText);
1231
1232 // Close modal after a short delay to show the success message
1233 setTimeout(function() {
1234 $('#mxchat_model_selector_modal').hide();
1235 // Restore original content and remove selection for next time
1236 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1237 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1238 $clickedCard.css('pointer-events', 'auto');
1239 }, 600);
1240 } else {
1241 // Show error state
1242 $clickedCard.find('.mxchat-model-selector-title').html(
1243 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1244 );
1245
1246 setTimeout(function() {
1247 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1248 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1249 $clickedCard.css('pointer-events', 'auto');
1250 }, 1500);
1251 }
1252 },
1253 error: function() {
1254 // Show error state
1255 $clickedCard.find('.mxchat-model-selector-title').html(
1256 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1257 );
1258
1259 setTimeout(function() {
1260 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1261 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1262 $clickedCard.css('pointer-events', 'auto');
1263 }, 1500);
1264 }
1265 });
1266 }, 300); // 300ms delay to show the selection before starting to save
1267 }
1268 });
1269
1270 // Close modal when clicking outside
1271 $(window).on('click', function(event) {
1272 if ($(event.target).is('#mxchat_model_selector_modal')) {
1273 $('#mxchat_model_selector_modal').hide();
1274 }
1275 });
1276 }
1277
1278 function loadOpenRouterModels() {
1279 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1280 const $modal = $('#mxchat_model_selector_modal');
1281 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1282
1283 if (!apiKey || apiKey.trim() === '') {
1284 // Show error message in modal
1285 $modalBody.html(`
1286 <div style="text-align: center; padding: 40px;">
1287 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1288 <h3>OpenRouter API Key Required</h3>
1289 <p>Please enter your OpenRouter API key in the settings before selecting a model. If you're seeing this message and recently entered API key, try refreshing.</p>
1290 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1291 </div>
1292 `);
1293
1294 $('#mxchat_back_to_models').on('click', function(e) {
1295 e.preventDefault();
1296 // CHANGE THIS: Instead of reloading, restore the original modal content
1297 $modalBody.html(`
1298 <div class="mxchat-model-selector-search-container">
1299 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1300 </div>
1301 <div class="mxchat-model-selector-categories">
1302 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1303 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1304 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1305 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1306 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1307 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1308 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1309 </div>
1310 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1311 `);
1312
1313 // Re-populate the grid
1314 populateModelsGrid('', 'all');
1315
1316 // Re-bind event handlers
1317 rebindModalEventHandlers();
1318 });
1319 return;
1320 }
1321
1322 // Show loading state
1323 $modalBody.html(`
1324 <div style="text-align: center; padding: 60px 20px;">
1325 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1326 <h3>Loading OpenRouter Models...</h3>
1327 <p>Fetching available models from OpenRouter</p>
1328 </div>
1329 `);
1330
1331 // Fetch models from OpenRouter
1332 jQuery.ajax({
1333 url: mxchatAdmin.ajax_url,
1334 type: 'POST',
1335 data: {
1336 action: 'mxchat_fetch_openrouter_models',
1337 api_key: apiKey,
1338 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1339 },
1340 success: function(response) {
1341 if (response.success && response.data.models) {
1342 displayOpenRouterModels(response.data.models);
1343 } else {
1344 $modalBody.html(`
1345 <div style="text-align: center; padding: 40px;">
1346 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1347 <h3>Error Loading Models</h3>
1348 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1349 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1350 </div>
1351 `);
1352
1353 $('#mxchat_back_to_models').on('click', function(e) {
1354 e.preventDefault();
1355 // CHANGE THIS: Restore original content instead of reloading
1356 restoreOriginalModalContent();
1357 });
1358 }
1359 },
1360 error: function() {
1361 $modalBody.html(`
1362 <div style="text-align: center; padding: 40px;">
1363 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1364 <h3>Connection Error</h3>
1365 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1366 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1367 </div>
1368 `);
1369
1370 $('#mxchat_back_to_models').on('click', function(e) {
1371 e.preventDefault();
1372 // CHANGE THIS: Restore original content instead of reloading
1373 restoreOriginalModalContent();
1374 });
1375 }
1376 });
1377 }
1378 function restoreOriginalModalContent() {
1379 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1380
1381 $modalBody.html(`
1382 <div class="mxchat-model-selector-search-container">
1383 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1384 </div>
1385 <div class="mxchat-model-selector-categories">
1386 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1387 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1388 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1389 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1390 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1391 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1392 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1393 </div>
1394 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1395 `);
1396
1397 // Re-populate the grid
1398 window.populateModelsGrid('', 'all');
1399
1400 // Re-bind event handlers
1401 rebindModalEventHandlers();
1402 }
1403 function rebindModalEventHandlers() {
1404 const $modal = $('#mxchat_model_selector_modal');
1405
1406 // Re-bind category button clicks
1407 $('.mxchat-model-category-btn').off('click').on('click', function() {
1408 $('.mxchat-model-category-btn').removeClass('active');
1409 $(this).addClass('active');
1410 const category = $(this).data('category');
1411 const searchTerm = $('#mxchat_model_search_input').val();
1412 window.populateModelsGrid(searchTerm, category);
1413 });
1414
1415 // Re-bind search input
1416 $('#mxchat_model_search_input').off('input').on('input', function() {
1417 const searchTerm = $(this).val();
1418 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1419 populateModelsGrid(searchTerm, activeCategory);
1420 });
1421 }
1422
1423 function restoreDefaultModalFooter() {
1424 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1425
1426 // Restore default footer buttons
1427 $modalFooter.html(`
1428 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1429 `);
1430
1431 // Re-bind cancel button
1432 $('#mxchat_cancel_model_selection').on('click', function() {
1433 $('#mxchat_model_selector_modal').hide();
1434 });
1435 }
1436
1437 function displayOpenRouterModels(models) {
1438 const $modal = $('#mxchat_model_selector_modal');
1439 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1440 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1441 const currentSelected = $('#openrouter_selected_model').val();
1442
1443 // Variable to store the currently selected model (in the UI, not yet saved)
1444 let pendingSelection = {
1445 modelId: currentSelected || null,
1446 modelName: $('#openrouter_selected_model_name').val() || null
1447 };
1448
1449 // Build new modal content with search and models
1450 const newContent = `
1451 <div class="mxchat-model-selector-search-container">
1452 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1453 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1454 <strong>${models.length} models available</strong> ·
1455 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1456 </p>
1457 </div>
1458 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1459 `;
1460
1461 // Update footer with Save button for OpenRouter
1462 const footerContent = `
1463 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1464 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1465 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1466 </button>
1467 `;
1468
1469 $modalBody.html(newContent);
1470 $modalFooter.html(footerContent);
1471
1472 // Function to render models
1473 function renderOpenRouterModels(filterText = '') {
1474 const $grid = $('#mxchat_openrouter_models_grid');
1475 $grid.empty();
1476
1477 let filteredModels = models;
1478 if (filterText) {
1479 const lowerFilter = filterText.toLowerCase();
1480 filteredModels = models.filter(m =>
1481 m.id.toLowerCase().includes(lowerFilter) ||
1482 m.name.toLowerCase().includes(lowerFilter) ||
1483 (m.description && m.description.toLowerCase().includes(lowerFilter))
1484 );
1485 }
1486
1487 filteredModels.forEach(model => {
1488 const isSelected = pendingSelection.modelId === model.id;
1489 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1490 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1491
1492 const $card = jQuery(`
1493 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1494 <div class="mxchat-model-selector-icon">
1495 ${getOpenRouterIcon(model.id)}
1496 </div>
1497 <div class="mxchat-model-selector-info">
1498 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1499 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1500 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1501 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1502 </div>
1503 </div>
1504 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1505 </div>
1506 `);
1507
1508 $grid.append($card);
1509 });
1510 }
1511
1512 // Helper to get icon
1513 function getOpenRouterIcon(modelId) {
1514 if (modelId.includes('gpt') || modelId.includes('openai')) {
1515 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
1516 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1517 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 176" fill="none" class="mxchat-model-icon-claude"><path fill="currentColor" d="m147.487 0l70.081 175.78H256L185.919 0zM66.183 106.221l23.98-61.774l23.98 61.774zM70.07 0L0 175.78h39.18l14.33-36.914h73.308l14.328 36.914h39.179L110.255 0z"></path></svg>';
1518 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1519 return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" class="mxchat-model-icon-gemini"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"></path></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"></use></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"></path><clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"></path><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"></path><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"></path></svg>';
1520 }
1521 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1522 }
1523
1524 // Initial render
1525 renderOpenRouterModels();
1526
1527 // Search handler
1528 $('#mxchat_openrouter_search').on('input', function() {
1529 renderOpenRouterModels($(this).val());
1530 });
1531
1532 $('#mxchat_back_to_provider_select').on('click', function(e) {
1533 e.preventDefault();
1534 // Restore footer to default state
1535 restoreDefaultModalFooter();
1536 // Instead of location.reload(), restore original content
1537 restoreOriginalModalContent();
1538 });
1539
1540 // Back to providers footer button
1541 $('#mxchat_back_to_models_footer').on('click', function(e) {
1542 e.preventDefault();
1543 // Restore footer to default state
1544 restoreDefaultModalFooter();
1545 // Restore original content
1546 restoreOriginalModalContent();
1547 });
1548
1549 // Model selection - just highlight, don't save yet
1550 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1551 e.stopPropagation(); // Prevent triggering the regular model card handler
1552
1553 const modelId = $(this).data('model-id');
1554 const modelName = $(this).data('model-name');
1555
1556 // Remove selection from all cards
1557 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1558
1559 // Add selection to clicked card
1560 $(this).addClass('mxchat-model-selected');
1561 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1562 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1563 }
1564
1565 // Update pending selection
1566 pendingSelection.modelId = modelId;
1567 pendingSelection.modelName = modelName;
1568
1569 // Enable the save button
1570 $('#mxchat_save_openrouter_model').prop('disabled', false);
1571 });
1572
1573 // Save button handler
1574 $('#mxchat_save_openrouter_model').on('click', function() {
1575 const $saveButton = $(this);
1576
1577 if (!pendingSelection.modelId) {
1578 return;
1579 }
1580
1581 // Disable button and show loading state
1582 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1583
1584 // First, save that we're using OpenRouter
1585 jQuery.ajax({
1586 url: mxchatAdmin.ajax_url,
1587 type: 'POST',
1588 data: {
1589 action: 'mxchat_save_setting',
1590 name: 'model',
1591 value: 'openrouter',
1592 _ajax_nonce: mxchatAdmin.setting_nonce
1593 },
1594 success: function() {
1595 // After model is set, save the model ID
1596 jQuery.ajax({
1597 url: mxchatAdmin.ajax_url,
1598 type: 'POST',
1599 data: {
1600 action: 'mxchat_save_setting',
1601 name: 'openrouter_selected_model',
1602 value: pendingSelection.modelId,
1603 _ajax_nonce: mxchatAdmin.setting_nonce
1604 },
1605 success: function() {
1606 // Update DOM immediately
1607 $('#openrouter_selected_model').val(pendingSelection.modelId);
1608
1609 // After model ID is saved, save the display name
1610 jQuery.ajax({
1611 url: mxchatAdmin.ajax_url,
1612 type: 'POST',
1613 data: {
1614 action: 'mxchat_save_setting',
1615 name: 'openrouter_selected_model_name',
1616 value: pendingSelection.modelName,
1617 _ajax_nonce: mxchatAdmin.setting_nonce
1618 },
1619 success: function() {
1620 // Update DOM immediately
1621 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1622
1623 // Update button text
1624 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1625
1626 // Update the "Currently using" message
1627 const $currentSelection = $('#openrouter-current-selection');
1628 if ($currentSelection.length) {
1629 $currentSelection.html(
1630 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1631 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1632 ).show();
1633 }
1634
1635 // Show success state briefly
1636 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1637
1638 // Close modal after short delay
1639 setTimeout(function() {
1640 // Restore footer to default state
1641 restoreDefaultModalFooter();
1642 $modal.hide();
1643 }, 800);
1644 },
1645 error: function() {
1646 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1647 alert('Failed to save model name. Please try again.');
1648 }
1649 });
1650 },
1651 error: function() {
1652 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1653 alert('Failed to save model ID. Please try again.');
1654 }
1655 });
1656 },
1657 error: function() {
1658 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1659 alert('Failed to save OpenRouter selection. Please try again.');
1660 }
1661 });
1662 });
1663
1664 }
1665
1666 // 3.2.3: Confirmation dialog shown when the user attempts to switch embedding
1667 // models after they've already embedded content with a different model. Mixing
1668 // embeddings from two models silently breaks similarity matching.
1669 function showEmbeddingSwitchWarning(data, newValue, onChoice) {
1670 $('#mxchat_embedding_switch_warning').remove();
1671
1672 const dimsBlock = data.dims_differ ? `
1673 <div class="mxchat-embed-warn-dims">
1674 <strong>Dimension mismatch:</strong>
1675 Existing vectors are ${data.active_dims}-dimensional, but ${data.new_label}
1676 produces ${data.new_dims}-dimensional vectors.
1677 If you use Pinecone, your index will reject queries entirely until you re-embed.
1678 </div>` : '';
1679
1680 const $modal = $(`
1681 <div id="mxchat_embedding_switch_warning" class="mxchat-embed-warn-overlay">
1682 <div class="mxchat-embed-warn-dialog">
1683 <div class="mxchat-embed-warn-header">
1684 <span class="mxchat-embed-warn-icon">⚠️</span>
1685 <h3>Switching embedding models will break similarity matching</h3>
1686 </div>
1687 <div class="mxchat-embed-warn-body">
1688 <p>
1689 Your knowledge base and actions are currently embedded with
1690 <code>${data.active_label}</code>. Switching to
1691 <code>${data.new_label}</code> means new queries are embedded with
1692 a different model than the stored vectors — the chatbot will return
1693 inaccurate results or fail to match anything.
1694 </p>
1695 ${dimsBlock}
1696 <p><strong>To switch safely:</strong></p>
1697 <ol>
1698 <li>Go to <em>Knowledge Base</em> and delete all existing entries.</li>
1699 <li>Go to <em>Actions</em> and delete all existing actions.</li>
1700 <li>Come back here, switch the model, then re-import your content and re-add your actions.</li>
1701 </ol>
1702 </div>
1703 <div class="mxchat-embed-warn-footer">
1704 <button type="button" class="button button-secondary" id="mxchat_embed_warn_cancel">Cancel — keep ${data.active_label}</button>
1705 <button type="button" class="button button-primary mxchat-embed-warn-danger" id="mxchat_embed_warn_continue">Switch anyway (I'll handle it)</button>
1706 </div>
1707 </div>
1708 </div>
1709 `);
1710
1711 $('body').append($modal);
1712
1713 let resolved = false;
1714 function resolve(confirmed) {
1715 if (resolved) return;
1716 resolved = true;
1717 $modal.remove();
1718 if (typeof onChoice === 'function') onChoice(confirmed);
1719 }
1720
1721 // Click on the overlay background dismisses (cancel)
1722 $modal.on('click', function(e) {
1723 if (e.target === $modal[0]) resolve(false);
1724 });
1725 $modal.find('#mxchat_embed_warn_cancel').on('click', function() { resolve(false); });
1726 $modal.find('#mxchat_embed_warn_continue').on('click', function() { resolve(true); });
1727 }
1728
1729 // Embedding model selector - completely separate from chat model selector
1730 function setupMxChatEmbeddingModelSelector() {
1731 const $embeddingModelSelect = $('#embedding_model');
1732
1733 // Skip if the element doesn't exist on the page
1734 if ($embeddingModelSelect.length === 0) {
1735 return;
1736 }
1737
1738 const $embeddingModelSelectorButton = $('<button>', {
1739 type: 'button',
1740 id: 'mxchat_embedding_model_selector_btn',
1741 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1742 text: 'Select Embedding Model'
1743 });
1744
1745 // Replace the select dropdown with a button
1746 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1747
1748 // Update button text to show currently selected model
1749 function updateButtonText() {
1750 const selectedModel = $embeddingModelSelect.val();
1751 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1752 $embeddingModelSelectorButton.text(selectedModelText);
1753 }
1754
1755 // Initialize button text
1756 updateButtonText();
1757
1758 // Create a unique ID for the modal to avoid conflicts
1759 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1760
1761 // Create and append modal HTML with unique IDs
1762 const embeddingModelSelectorModal = `
1763 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1764 <div class="mxchat-embedding-model-selector-modal-content">
1765 <div class="mxchat-embedding-model-selector-modal-header">
1766 <h3>Select Embedding Model</h3>
1767 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1768 </div>
1769 <div class="mxchat-embedding-model-selector-modal-body">
1770 <div class="mxchat-embedding-model-selector-search-container">
1771 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1772 </div>
1773 <div class="mxchat-embedding-model-selector-categories">
1774 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1775 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1776 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1777 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1778 </div>
1779 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1780 </div>
1781 <div class="mxchat-embedding-model-selector-modal-footer">
1782 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1783 </div>
1784 </div>
1785 </div>
1786 `;
1787
1788 // Use jQuery's append to ensure it doesn't clash with existing modals
1789 $('body').append(embeddingModelSelectorModal);
1790
1791 // Populate models grid
1792 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1793 const $grid = $('#mxchat_embedding_models_grid');
1794 $grid.empty();
1795
1796 // Define embedding models with descriptions and context lengths
1797 const models = {
1798 openai: [
1799 {
1800 value: 'text-embedding-3-small',
1801 label: 'TE3 Small',
1802 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1803 },
1804 {
1805 value: 'text-embedding-ada-002',
1806 label: 'Ada 2',
1807 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1808 },
1809 {
1810 value: 'text-embedding-3-large',
1811 label: 'TE3 Large',
1812 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1813 }
1814 ],
1815 voyage: [
1816 {
1817 value: 'voyage-3-large',
1818 label: 'Voyage-3 Large',
1819 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1820 }
1821 ],
1822 gemini: [
1823 {
1824 value: 'gemini-embedding-001',
1825 label: 'Gemini Embedding',
1826 description: 'Stable SOTA embeddings (1536 dimensions, 8K context)'
1827 }
1828 ]
1829 };
1830
1831 let allModels = [];
1832 Object.keys(models).forEach(key => {
1833 if (category === 'all' || category === key) {
1834 allModels = allModels.concat(models[key]);
1835 }
1836 });
1837
1838 // Filter by search term if present
1839 if (filter) {
1840 const lowerFilter = filter.toLowerCase();
1841 allModels = allModels.filter(model =>
1842 model.label.toLowerCase().includes(lowerFilter) ||
1843 model.description.toLowerCase().includes(lowerFilter)
1844 );
1845 }
1846
1847 // Create model cards
1848 allModels.forEach(model => {
1849 const isSelected = $embeddingModelSelect.val() === model.value;
1850 let providerClass = 'mxchat-embedding-model-provider-openai';
1851
1852 if (model.value.startsWith('voyage-')) {
1853 providerClass = 'mxchat-embedding-model-provider-voyage';
1854 } else if (model.value.startsWith('gemini-embedding-')) {
1855 providerClass = 'mxchat-embedding-model-provider-gemini';
1856 }
1857
1858 let iconHTML = '';
1859 if (model.value.startsWith('voyage-')) {
1860 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1861 } else if (model.value.startsWith('gemini-embedding-')) {
1862 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1863 } else {
1864 iconHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-embedding-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
1865 }
1866
1867 const $modelCard = $(`
1868 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1869 <div class="mxchat-embedding-model-selector-icon">
1870 ${iconHTML}
1871 </div>
1872 <div class="mxchat-embedding-model-selector-info">
1873 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1874 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1875 </div>
1876 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1877 </div>
1878 `);
1879
1880 $grid.append($modelCard);
1881 });
1882 }
1883
1884 // Event handlers - use namespaced events to avoid conflicts
1885 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1886 e.stopPropagation(); // Prevent event bubbling
1887 $('#' + embeddingModalId).show();
1888 populateEmbeddingModelsGrid('', 'all');
1889 });
1890
1891 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1892 e.stopPropagation(); // Prevent event bubbling
1893 $('#' + embeddingModalId).hide();
1894 });
1895
1896 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1897 e.stopPropagation(); // Prevent event bubbling
1898 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1899 $(this).addClass('active');
1900 const category = $(this).data('category');
1901 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1902 populateEmbeddingModelsGrid(searchTerm, category);
1903 });
1904
1905 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1906 const searchTerm = $(this).val();
1907 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1908 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1909 });
1910
1911 // 3.2.3: Commit a model change — same as the original click handler, just
1912 // factored out so it can be invoked from both the safe path and the
1913 // post-confirmation path.
1914 function commitEmbeddingModelChange(modelValue) {
1915 $embeddingModelSelect.val(modelValue);
1916 const changeEvent = new Event('change', { bubbles: true });
1917 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1918 updateButtonText();
1919 $('#' + embeddingModalId).hide();
1920 }
1921
1922 // Use a direct selector to avoid conflicts with other card elements
1923 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1924 e.stopPropagation(); // Prevent event bubbling
1925 const modelValue = $(this).data('value');
1926 const previousValue = $embeddingModelSelect.val();
1927
1928 // No-op if the user clicked the already-selected card
1929 if (modelValue === previousValue) {
1930 $('#' + embeddingModalId).hide();
1931 return;
1932 }
1933
1934 // 3.2.3: Preflight — if the user has content embedded with a different
1935 // model, surface a confirmation dialog before committing the switch.
1936 $.post(ajaxurl, {
1937 action: 'mxchat_check_embedding_switch',
1938 security: (typeof mxchatAdmin !== 'undefined' ? mxchatAdmin.nonce : ''),
1939 new_model: modelValue
1940 }).done(function(resp) {
1941 if (resp && resp.success && resp.data && resp.data.is_mismatch) {
1942 showEmbeddingSwitchWarning(resp.data, modelValue, function(confirmed) {
1943 if (confirmed) {
1944 commitEmbeddingModelChange(modelValue);
1945 }
1946 });
1947 } else {
1948 commitEmbeddingModelChange(modelValue);
1949 }
1950 }).fail(function() {
1951 // If preflight fails, fall back to the original behavior so a network
1952 // hiccup doesn't block legitimate model switches.
1953 commitEmbeddingModelChange(modelValue);
1954 });
1955 return;
1956 });
1957
1958 // Close modal when clicking outside - use namespaced events
1959 $(window).on('click.embeddingModelSelector', function(event) {
1960 if ($(event.target).is('#' + embeddingModalId)) {
1961 $('#' + embeddingModalId).hide();
1962 }
1963 });
1964 }
1965
1966 // Call this function after the DOM is fully loaded
1967 $(document).ready(function() {
1968 setupMxChatModelSelector();
1969 setupMxChatEmbeddingModelSelector();
1970 });
1971
1972 // Add Intent Form Submission
1973 $('#mxchat-add-intent-form').on('submit', function(event) {
1974 $('#mxchat-intent-loading').show();
1975 $('#mxchat-intent-loading-text').show();
1976 $(this).find('button[type="submit"]').hide();
1977 });
1978
1979 // Inline Edit Functionality
1980 $('.edit-button').on('click', function() {
1981 var row = $(this).closest('tr');
1982
1983 // Clear URL field if it's a manual content URL (mxchat:// protocol)
1984 var urlEdit = row.find('.url-edit');
1985 if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
1986 urlEdit.val('');
1987 }
1988
1989 // Expand the accordion to show the edit textarea (fixes short content editing)
1990 var contentFull = row.find('.mxchat-content-full');
1991 if (contentFull.length && contentFull.is(':hidden')) {
1992 contentFull.show();
1993 row.find('.mxchat-content-preview').hide();
1994 }
1995
1996 row.find('.content-view, .url-view').hide();
1997 row.find('.content-edit, .url-edit').show();
1998 row.find('.edit-button').hide();
1999 row.find('.save-button').show();
2000 });
2001
2002 // Save button handler
2003 // Save button handler
2004 $('.save-button').on('click', function() {
2005 var button = $(this);
2006 var row = button.closest('tr');
2007 var id = button.data('id');
2008 var nonce = button.data('nonce'); // Get nonce from button data attribute
2009 var newContent = row.find('.content-edit').val();
2010 var newUrl = row.find('.url-edit').val();
2011
2012 //console.log('Nonce from button:', nonce); // Debug
2013
2014 button.prop('disabled', true);
2015 button.text('Saving...');
2016
2017 $.ajax({
2018 url: mxchatAdmin.ajax_url,
2019 type: 'POST',
2020 data: {
2021 action: 'mxchat_save_inline_prompt',
2022 id: id,
2023 article_content: newContent,
2024 article_url: newUrl,
2025 _ajax_nonce: nonce // Use nonce from button
2026 },
2027 success: function(response) {
2028 button.prop('disabled', false);
2029 button.text('Save');
2030
2031 if (response.success) {
2032 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
2033 if (newUrl) {
2034 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
2035 } else {
2036 row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
2037 }
2038
2039 row.find('.content-edit, .url-edit').hide();
2040 row.find('.content-view, .url-view').show();
2041 row.find('.save-button').hide();
2042 row.find('.edit-button').show();
2043
2044 // Restore accordion state - show preview, hide full content
2045 row.find('.mxchat-content-preview').show();
2046 row.find('.mxchat-content-full').hide();
2047 } else {
2048 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
2049 }
2050 },
2051 error: function() {
2052 button.prop('disabled', false);
2053 button.text('Save');
2054 alert('An error occurred while saving.');
2055 }
2056 });
2057 });
2058
2059
2060 // Questions handling
2061 $('.mxchat-add-question').on('click', function () {
2062 const container = $('#mxchat-additional-questions-container');
2063 const questionCount = container.find('.mxchat-question-row').length + 4;
2064 const questionIndex = container.find('.mxchat-question-row').length;
2065
2066 const newQuestion = `
2067 <div class="mxchat-question-row">
2068 <input type="text"
2069 name="additional_popular_questions[]"
2070 placeholder="Enter Additional Popular Question ${questionCount}"
2071 class="regular-text mxchat-question-input"
2072 data-question-index="${questionIndex}" />
2073 <button type="button" class="button mxchat-remove-question"
2074 aria-label="Remove question">Remove</button>
2075 </div>
2076 `;
2077 container.append(newQuestion);
2078 });
2079
2080 $(document).on('click', '.mxchat-remove-question', function () {
2081 $(this).closest('.mxchat-question-row').remove();
2082 saveQuestions();
2083 });
2084
2085 $(document).on('change', '.mxchat-question-input', function() {
2086 saveQuestions();
2087 });
2088
2089 function saveQuestions() {
2090 const questions = [];
2091 $('.mxchat-question-input').each(function() {
2092 const value = $(this).val().trim();
2093 if (value) {
2094 questions.push(value);
2095 }
2096 });
2097
2098 const feedbackContainer = $('<div class="feedback-container"></div>');
2099 const spinner = $('<div class="saving-spinner"></div>');
2100 const successIcon = $('<div class="success-icon">✔</div>');
2101
2102 // Append feedback after the add button
2103 $('.mxchat-add-question').after(feedbackContainer);
2104 feedbackContainer.append(spinner);
2105
2106 // Save via AJAX
2107 $.ajax({
2108 url: mxchatAdmin.ajax_url,
2109 type: 'POST',
2110 data: {
2111 action: 'mxchat_save_setting',
2112 name: 'additional_popular_questions',
2113 value: JSON.stringify(questions),
2114 _ajax_nonce: mxchatAdmin.setting_nonce
2115 },
2116 success: function(response) {
2117 if (response.success) {
2118 spinner.fadeOut(200, function() {
2119 feedbackContainer.append(successIcon);
2120 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
2121 feedbackContainer.remove();
2122 });
2123 });
2124 } else {
2125 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
2126 feedbackContainer.remove();
2127 }
2128 },
2129 error: function() {
2130 alert('An error occurred while saving questions.');
2131 feedbackContainer.remove();
2132 }
2133 });
2134 }
2135
2136 // Live agent status handler
2137 const statusToggle = document.getElementById('live_agent_status');
2138 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
2139 if (statusToggle && statusText) {
2140 statusToggle.addEventListener('change', function() {
2141 // Update display text
2142 statusText.textContent = this.checked ? 'Online' : 'Offline';
2143
2144 // Send the correct on/off value to the server
2145 if (window.mxchatSaveSetting) {
2146 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
2147 }
2148 });
2149 }
2150
2151 // Live agent availability schedules (plans 8ccaa2 + 99d7a4).
2152 // The editor markup exists once PER CHANNEL (Slack + Telegram tabs), so
2153 // everything is scoped inside each .mxchat-la-schedule container via classes
2154 // — no page-global ids. Each editor collects its own day grid into its own
2155 // hidden live_agent_schedule_<channel> input as JSON and fires one change
2156 // event, so the existing autosave handler above does the POST.
2157 $('.mxchat-la-schedule').each(function() {
2158 const $laSchedule = $(this);
2159 const $laEnabled = $laSchedule.find('.mxchat-la-enabled');
2160 const $laHidden = $laSchedule.find('.mxchat-la-hidden');
2161 const $laDays = $laSchedule.find('.mxchat-la-days');
2162 const $laStatus = $laSchedule.find('.mxchat-la-status-text');
2163
2164 function laCollect() {
2165 const days = {};
2166 $laDays.find('.mxchat-la-day').each(function() {
2167 const $day = $(this);
2168 const n = $day.data('day');
2169 days[n] = {
2170 enabled: $day.find('.mxchat-la-day-enabled').is(':checked'),
2171 start: $day.find('.mxchat-la-start').val() || '09:00',
2172 end: $day.find('.mxchat-la-end').val() || '17:00'
2173 };
2174 });
2175 return { enabled: $laEnabled.is(':checked'), days: days };
2176 }
2177
2178 function laSync() {
2179 const schedule = laCollect();
2180 $laSchedule.toggleClass('is-active', schedule.enabled);
2181 $laDays.attr('aria-hidden', schedule.enabled ? 'false' : 'true');
2182 $laStatus.text(
2183 schedule.enabled
2184 ? (mxchatAdmin.i18n_scheduled_hours || 'Scheduled hours')
2185 : (mxchatAdmin.i18n_always_available || 'Always available')
2186 );
2187 $laDays.find('.mxchat-la-day').each(function() {
2188 const $day = $(this);
2189 $day.toggleClass('is-on', $day.find('.mxchat-la-day-enabled').is(':checked'));
2190 });
2191 // Hand this channel's schedule to the shared autosave transport.
2192 $laHidden.val(JSON.stringify(schedule)).trigger('change');
2193 }
2194
2195 $laSchedule.find('.mxchat-la-field').on('change', laSync);
2196 });
2197
2198 // Function to adjust the textarea height to content
2199 function adjustTextareaHeight() {
2200 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2201 this.style.height = this.scrollHeight + 'px'; // Expand to content height
2202 }
2203
2204 // Function to reset the textarea height to initial
2205 function resetTextareaHeight() {
2206 this.style.height = ''; // Remove inline height, reverting to CSS default
2207 }
2208
2209 // Target the specific textarea by ID
2210 var $textarea = $('#system_prompt_instructions');
2211
2212 // Bind events
2213 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2214 .on('blur', resetTextareaHeight); // Reset on blur
2215 });
2216
2217
2218
2219 document.addEventListener('DOMContentLoaded', function() {
2220 // Check if we're on the correct page before initializing
2221 const modal = document.getElementById('mxchat-action-modal');
2222
2223 // Only initialize if the modal exists on this page
2224 if (modal) {
2225 //console.log('MXChat Action Modal JS Loaded');
2226
2227 // Initialize the action modal functionality
2228 initStepBasedActionModal();
2229 }
2230
2231 // Function to initialize the step-based action modal
2232 function initStepBasedActionModal() {
2233 // We already checked for modal existence above, so no need to check again
2234
2235 const actionStep1 = document.getElementById('mxchat-action-step-1');
2236 const actionStep2 = document.getElementById('mxchat-action-step-2');
2237 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2238 const searchInput = document.getElementById('action-type-search');
2239 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2240 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2241 const actionForm = document.getElementById('mxchat-action-form');
2242 const callbackInput = document.getElementById('callback_function');
2243 const actionIdField = document.getElementById('edit_action_id');
2244 const labelField = document.getElementById('intent_label');
2245 const phrasesField = document.getElementById('action_phrases');
2246 const formActionType = document.getElementById('form_action_type');
2247 const nonceContainer = document.getElementById('action-nonce-container');
2248 const thresholdSlider = document.getElementById('similarity_threshold');
2249 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2250
2251 // Rest of your initialization code remains the same...
2252
2253 // Log the structure of one action card for debugging
2254 if (actionCards.length > 0) {
2255 //console.log('First action card data attributes:', actionCards[0].dataset);
2256 //console.log('First action card HTML:', actionCards[0].outerHTML);
2257 }
2258
2259 // Add click event listeners to category buttons
2260 categoryButtons.forEach(button => {
2261 button.addEventListener('click', function() {
2262 //console.log('Category button clicked:', this.dataset.category);
2263
2264 // Remove active class from all buttons
2265 categoryButtons.forEach(btn => btn.classList.remove('active'));
2266
2267 // Add active class to clicked button
2268 this.classList.add('active');
2269
2270 // Get selected category
2271 const category = this.dataset.category;
2272
2273 // Filter action cards
2274 filterActionCards(category, searchInput.value);
2275 });
2276 });
2277
2278 // Add search functionality
2279 if (searchInput) {
2280 searchInput.addEventListener('input', function() {
2281 // Get active category
2282 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2283 //console.log('Search input changed, active category:', activeCategory);
2284
2285 // Filter action cards
2286 filterActionCards(activeCategory, this.value);
2287 });
2288 }
2289
2290 // Add click event listeners to action cards
2291 actionCards.forEach(card => {
2292 card.addEventListener('click', function() {
2293 // Get the action data
2294 const isPro = this.dataset.pro === 'true';
2295 const isInstalled = this.dataset.installed === 'true';
2296 const addonName = this.dataset.addon || '';
2297 const actionValue = this.dataset.value;
2298 const actionLabel = this.dataset.label;
2299 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2300 const actionDescription = this.querySelector('p').textContent;
2301
2302 // Check if this is a promotional card (add-on not installed)
2303 const isPromo = this.dataset.promo === 'true';
2304
2305 if (isPromo || (addonName && !isInstalled)) {
2306 // Add-on required but not installed — show informational notice
2307 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '').replace('— Get Add-on', '').trim() || addonName + ' Add-on';
2308 showAddonRequiredNotice(addonDisplayName);
2309 return;
2310 }
2311
2312 // If we get here, the action is available - proceed as normal
2313 callbackInput.value = actionValue;
2314
2315 // Update the selected action display in step 2
2316 document.getElementById('selected-action-title').textContent = actionLabel;
2317 document.getElementById('selected-action-description').textContent = actionDescription;
2318 document.getElementById('selected-action-icon').innerHTML =
2319 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2320
2321 // Set a default label based on the action type (user can change it)
2322 if (!labelField.value) {
2323 labelField.value = actionLabel;
2324 }
2325
2326 // Move to step 2
2327 actionStep1.classList.remove('active');
2328 actionStep2.classList.add('active');
2329
2330 // Update modal title
2331 });
2332 });
2333
2334 // Back button functionality
2335 if (backToStep1Btn) {
2336 backToStep1Btn.addEventListener('click', function() {
2337 //console.log('Back button clicked');
2338 actionStep2.classList.remove('active');
2339 actionStep1.classList.add('active');
2340 });
2341 }
2342
2343 // Function to filter action cards by category and search term
2344 function filterActionCards(category, searchTerm) {
2345 searchTerm = searchTerm.toLowerCase().trim();
2346 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2347
2348 let visibleCount = 0;
2349
2350 // Show all cards initially with animation
2351 actionCards.forEach((card, index) => {
2352 // Reset animation
2353 card.style.animation = 'none';
2354 // Trigger reflow
2355 void card.offsetWidth;
2356
2357 // Determine if card should be visible based on category and search term
2358 const cardCategory = card.dataset.category || '';
2359 const matchesCategory = category === 'all' || cardCategory === category;
2360
2361 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2362 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2363 const matchesSearch = searchTerm === '' ||
2364 cardTitle.includes(searchTerm) ||
2365 cardDesc.includes(searchTerm);
2366
2367 // Show/hide card with animation
2368 if (matchesCategory && matchesSearch) {
2369 card.style.display = 'flex';
2370 // Staggered animation for cards
2371 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2372 visibleCount++;
2373 } else {
2374 card.style.display = 'none';
2375 }
2376 });
2377
2378 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2379 }
2380
2381 // Function to show notice for Pro features
2382 function showProFeatureNotice() {
2383 //console.log('Showing Pro feature notice');
2384 // Check if we already have a notification container
2385 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2386
2387 if (!noticeContainer) {
2388 // Create the notice container
2389 noticeContainer = document.createElement('div');
2390 noticeContainer.className = 'mxchat-pro-notice';
2391
2392 // Create content
2393 noticeContainer.innerHTML = `
2394 <div class="mxchat-pro-notice-content">
2395 <h3>MxChat Pro Feature</h3>
2396 <p>This action is available in the Pro version only.</p>
2397 <div class="mxchat-pro-notice-buttons">
2398 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2399 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2400 </div>
2401 </div>
2402 `;
2403
2404 // Append to body
2405 document.body.appendChild(noticeContainer);
2406
2407 // Add close functionality
2408 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2409 closeButton.addEventListener('click', function() {
2410 noticeContainer.classList.remove('active');
2411 setTimeout(() => {
2412 noticeContainer.remove();
2413 }, 300);
2414 });
2415
2416 // Click outside to close (drag-safe)
2417 mxchatDragSafeOverlayClose(noticeContainer, function() {
2418 closeButton.click();
2419 });
2420
2421 // Show with animation
2422 setTimeout(() => {
2423 noticeContainer.classList.add('active');
2424 }, 10);
2425 } else {
2426 // If it already exists, just make it visible again
2427 noticeContainer.classList.add('active');
2428 }
2429 }
2430
2431 // Function to show notice for add-on requirements
2432 function showAddonRequiredNotice(addonName) {
2433 //console.log(`Showing add-on notice for: ${addonName}`);
2434 // Check if we already have a notification container
2435 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2436
2437 if (!noticeContainer) {
2438 // Create the notice container
2439 noticeContainer = document.createElement('div');
2440 noticeContainer.className = 'mxchat-addon-notice';
2441
2442 // Create content
2443 noticeContainer.innerHTML = `
2444 <div class="mxchat-addon-notice-content">
2445 <span class="mxchat-addon-notice-icon">🧩</span>
2446 <h3>Add-on Required</h3>
2447 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2448 <div class="mxchat-addon-notice-buttons">
2449 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2450 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2451 </div>
2452 </div>
2453 `;
2454
2455 // Append to body
2456 document.body.appendChild(noticeContainer);
2457
2458 // Add close functionality
2459 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2460 closeButton.addEventListener('click', function() {
2461 noticeContainer.classList.remove('active');
2462 setTimeout(() => {
2463 noticeContainer.remove();
2464 }, 300);
2465 });
2466
2467 // Click outside to close (drag-safe)
2468 mxchatDragSafeOverlayClose(noticeContainer, function() {
2469 closeButton.click();
2470 });
2471
2472 // Show with animation
2473 setTimeout(() => {
2474 noticeContainer.classList.add('active');
2475 }, 10);
2476 } else {
2477 // If it already exists, update the content
2478 const addonNameElement = noticeContainer.querySelector('p strong');
2479 if (addonNameElement) {
2480 addonNameElement.textContent = addonName;
2481 }
2482
2483 // Make it visible again
2484 noticeContainer.classList.add('active');
2485 }
2486 }
2487
2488 // Form submission handling
2489 if (actionForm) {
2490 actionForm.addEventListener('submit', function() {
2491 //console.log('Form submitted');
2492 document.getElementById('mxchat-action-loading').style.display = 'flex';
2493 this.querySelector('button[type="submit"]').disabled = true;
2494 });
2495 }
2496 }
2497
2498 // Setup add action buttons (only if we're on the correct page)
2499 if (modal) {
2500 // Update the modal open function to support the step-based flow
2501 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2502 //console.log('Modal opening, edit mode:', isEdit);
2503
2504 // Get form fields
2505 const actionIdField = document.getElementById('edit_action_id');
2506 const labelField = document.getElementById('intent_label');
2507 const phrasesField = document.getElementById('action_phrases');
2508 const formActionType = document.getElementById('form_action_type');
2509 const callbackInput = document.getElementById('callback_function');
2510 const saveButton = document.getElementById('mxchat-save-action-btn');
2511 const nonceContainer = document.getElementById('action-nonce-container');
2512 const thresholdSlider = document.getElementById('similarity_threshold');
2513 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2514 const actionStep1 = document.getElementById('mxchat-action-step-1');
2515 const actionStep2 = document.getElementById('mxchat-action-step-2');
2516 const searchInput = document.getElementById('action-type-search');
2517
2518 // Set up modal for edit or create
2519 if (isEdit) {
2520 saveButton.textContent = 'Update Action';
2521 formActionType.value = 'mxchat_edit_intent';
2522 actionIdField.value = actionId;
2523 labelField.value = label;
2524 phrasesField.value = phrases;
2525 callbackInput.value = callbackFunction;
2526 thresholdSlider.value = threshold; // Set the current threshold value
2527 thresholdDisplay.textContent = threshold + '%'; // Update display
2528
2529 // NEW: Handle bot selection checkboxes for editing
2530 // First uncheck all bot checkboxes
2531 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2532 checkbox.checked = false;
2533 });
2534
2535 // Then check the ones that should be enabled
2536 if (enabledBots) {
2537 let botsArray;
2538 if (typeof enabledBots === 'string') {
2539 try {
2540 botsArray = JSON.parse(enabledBots);
2541 } catch (e) {
2542 console.warn('Failed to parse enabledBots:', enabledBots);
2543 botsArray = ['default']; // fallback
2544 }
2545 } else if (Array.isArray(enabledBots)) {
2546 botsArray = enabledBots;
2547 } else {
2548 botsArray = ['default']; // fallback
2549 }
2550
2551 botsArray.forEach(botId => {
2552 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2553 if (checkbox) {
2554 checkbox.checked = true;
2555 }
2556 });
2557 } else {
2558 // Fallback to default if no bot data
2559 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2560 if (defaultCheckbox) {
2561 defaultCheckbox.checked = true;
2562 }
2563 }
2564
2565 // Update the nonce field for editing
2566 nonceContainer.innerHTML = ''; // Clear existing nonce
2567 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2568 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2569 }
2570
2571 // For editing, go directly to step 2 and update the selected action display
2572 actionStep1.classList.remove('active');
2573 actionStep2.classList.add('active');
2574
2575 // Find the matching action card to get its details
2576 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2577 let foundCard = null;
2578
2579 actionCards.forEach(card => {
2580 if (card.dataset.value === callbackFunction) {
2581 foundCard = card;
2582 }
2583 });
2584
2585 if (foundCard) {
2586 //console.log('Found matching action card for:', callbackFunction);
2587 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2588 const actionIconElement = foundCard.querySelector('.dashicons');
2589 const actionIcon = actionIconElement
2590 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2591 : 'admin-generic';
2592 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2593
2594 document.getElementById('selected-action-title').textContent = actionLabel;
2595 document.getElementById('selected-action-description').textContent = actionDescription;
2596 document.getElementById('selected-action-icon').innerHTML =
2597 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2598 } else {
2599 //console.log('No matching action card found for:', callbackFunction);
2600 // Fallback if we can't find the card
2601 document.getElementById('selected-action-title').textContent = label;
2602 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2603 document.getElementById('selected-action-icon').innerHTML =
2604 `<span class="dashicons dashicons-admin-generic"></span>`;
2605 }
2606 } else {
2607 //console.log('Setting up create mode');
2608 saveButton.textContent = 'Save Action';
2609 formActionType.value = 'mxchat_add_intent';
2610 actionIdField.value = '';
2611 labelField.value = '';
2612 phrasesField.value = '';
2613 callbackInput.value = '';
2614 thresholdSlider.value = 85; // Default value for new actions
2615 thresholdDisplay.textContent = '85%'; // Default display
2616
2617 // For new actions, ensure default is checked and others are unchecked
2618 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2619 checkbox.checked = (checkbox.value === 'default');
2620 });
2621
2622 // Update the nonce field for adding
2623 nonceContainer.innerHTML = ''; // Clear existing nonce
2624 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2625 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2626 }
2627
2628 // For creating new, start at step 1
2629 actionStep1.classList.add('active');
2630 actionStep2.classList.remove('active');
2631 }
2632
2633 // Show modal with animation
2634 modal.style.display = 'flex';
2635 requestAnimationFrame(() => {
2636 modal.classList.add('active');
2637 });
2638
2639 // Set up close handlers
2640 let detachEsc = null;
2641 const closeModal = () => {
2642 //console.log('Closing modal');
2643 if (detachEsc) {
2644 detachEsc();
2645 detachEsc = null;
2646 }
2647 modal.classList.remove('active');
2648 setTimeout(() => {
2649 modal.style.display = 'none';
2650 }, 300); // Match the CSS transition time
2651 };
2652
2653 // This modal holds edits: close only on explicit controls, confirming when dirty.
2654 const guardedClose = mxchatGuardedClose(modal, closeModal);
2655
2656 // Close button handler
2657 const closeBtn = modal.querySelector('.mxchat-modal-close');
2658 if (closeBtn) {
2659 closeBtn.onclick = guardedClose;
2660 }
2661
2662 // Cancel button handler
2663 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2664 if (cancelBtns) {
2665 cancelBtns.forEach(btn => {
2666 btn.onclick = guardedClose;
2667 });
2668 }
2669
2670 // Escape key to close modal
2671 detachEsc = mxchatBindEscClose(modal, guardedClose);
2672
2673 // Focus appropriate field based on current step
2674 if (isEdit || actionStep2.classList.contains('active')) {
2675 if (labelField) labelField.focus();
2676 } else {
2677 if (searchInput) searchInput.focus();
2678 }
2679
2680 return closeModal; // Return close function for external use
2681 };
2682 // Setup add action buttons
2683 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2684 if (addActionBtn) {
2685 //console.log('Add action button found');
2686 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2687 }
2688
2689 const createFirstAction = document.getElementById('mxchat-create-first-action');
2690 if (createFirstAction) {
2691 //console.log('Create first action button found');
2692 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2693 }
2694
2695 // Setup edit buttons
2696 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2697 //console.log('Edit buttons found:', editButtons.length);
2698 editButtons.forEach(button => {
2699 button.onclick = () => {
2700 const actionId = button.dataset.actionId;
2701 const phrases = button.dataset.phrases;
2702 const label = button.dataset.label;
2703 const threshold = button.dataset.threshold || 85;
2704 const callbackFunction = button.dataset.callbackFunction;
2705 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2706
2707 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2708 };
2709 });
2710 }
2711 });
2712
2713 jQuery(document).ready(function($) {
2714 // Auto-expand custom post types container if any are checked
2715 // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2716 function autoExpandIfNeeded() {
2717 const $container = $('#mxchat-custom-post-types-container');
2718 const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2719
2720 if ($container.length === 0) return;
2721
2722 const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2723
2724 if (hasCheckedItems) {
2725 $container.show();
2726 const $icon = $toggleBtn.find('span:last-child');
2727 if ($icon.length) {
2728 $icon.text('');
2729 }
2730 }
2731 }
2732
2733 // Run on page load
2734 autoExpandIfNeeded();
2735 });
2736
2737 document.addEventListener('DOMContentLoaded', function() {
2738 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2739 var modal = document.getElementById('mxchatSampleModal');
2740 var modalClose = document.getElementById('mxchatModalClose');
2741 var closeBtn = document.getElementById('mxchatCloseBtn');
2742 var copyBtn = document.getElementById('mxchatCopyBtn');
2743 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2744 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2745
2746 if (!viewSampleBtn || !modal) {
2747 return;
2748 }
2749
2750 // Open modal
2751 viewSampleBtn.addEventListener('click', function(e) {
2752 e.preventDefault();
2753 e.stopPropagation();
2754 modal.classList.add('mxchat-instructions-show');
2755 });
2756
2757 // Close modal function
2758 function closeModal(e) {
2759 if (e) {
2760 e.preventDefault();
2761 e.stopPropagation();
2762 }
2763 modal.classList.remove('mxchat-instructions-show');
2764 }
2765
2766 // Close modal events
2767 if (modalClose) {
2768 modalClose.addEventListener('click', function(e) {
2769 closeModal(e);
2770 });
2771 }
2772
2773 if (closeBtn) {
2774 closeBtn.addEventListener('click', function(e) {
2775 closeModal(e);
2776 });
2777 }
2778
2779 // Close on backdrop click ONLY (not on hover), and only when the whole gesture
2780 // happened on the backdrop — selecting the sample text and releasing past the
2781 // dialog edge otherwise dispatches the click on the overlay and closes it.
2782 mxchatDragSafeOverlayClose(modal, closeModal);
2783
2784 // Prevent modal content clicks from closing the modal
2785 if (modalContent) {
2786 modalContent.addEventListener('click', function(e) {
2787 e.stopPropagation();
2788 });
2789 }
2790
2791 // Close on escape key
2792 document.addEventListener('keydown', function(e) {
2793 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2794 closeModal();
2795 }
2796 });
2797
2798 // Copy functionality
2799 if (copyBtn && instructionsContent) {
2800 copyBtn.addEventListener('click', function(e) {
2801 e.preventDefault();
2802 e.stopPropagation();
2803
2804 var text = instructionsContent.textContent;
2805
2806 if (navigator.clipboard) {
2807 navigator.clipboard.writeText(text).then(function() {
2808 showCopySuccess();
2809 }).catch(function() {
2810 fallbackCopy(text);
2811 });
2812 } else {
2813 fallbackCopy(text);
2814 }
2815 });
2816 }
2817
2818 function fallbackCopy(text) {
2819 var textArea = document.createElement('textarea');
2820 textArea.value = text;
2821 textArea.style.position = 'fixed';
2822 textArea.style.left = '-999999px';
2823 textArea.style.top = '-999999px';
2824 document.body.appendChild(textArea);
2825 textArea.select();
2826 try {
2827 document.execCommand('copy');
2828 showCopySuccess();
2829 } catch (err) {
2830 console.error('Copy failed');
2831 }
2832 document.body.removeChild(textArea);
2833 }
2834
2835 function showCopySuccess() {
2836 var originalText = copyBtn.innerHTML;
2837 copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,6 9,17 4,12"/></svg>Copied!';
2838
2839 setTimeout(function() {
2840 copyBtn.innerHTML = originalText;
2841 }, 2000);
2842 }
2843 });
2844
2845 jQuery(document).ready(function($) {
2846 var emailDependentFields = [
2847 '#email_blocker_header_content',
2848 '#email_blocker_button_text',
2849 '#enable_name_field',
2850 '#name_field_placeholder'
2851 ];
2852
2853 // Add visual indicators
2854 emailDependentFields.forEach(function(fieldId) {
2855 var $row = $(fieldId).closest('tr');
2856 $row.addClass('email-dependent-field');
2857
2858 // Add an icon to show it's dependent
2859 var $label = $row.find('th label, th');
2860 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2861 });
2862
2863 // Hide initially with opacity for smoother transition
2864 emailDependentFields.forEach(function(fieldId) {
2865 $(fieldId).closest('tr').hide().css('opacity', '0');
2866 });
2867
2868 function toggleEmailFields() {
2869 var emailEnabled = $('#enable_email_block').is(':checked');
2870
2871 emailDependentFields.forEach(function(fieldId) {
2872 var $row = $(fieldId).closest('tr');
2873 if (emailEnabled) {
2874 $row.slideDown(400).animate({opacity: 1}, 200);
2875 } else {
2876 $row.animate({opacity: 0}, 200).slideUp(400);
2877 }
2878 });
2879 }
2880
2881 toggleEmailFields();
2882 $('#enable_email_block').on('change', toggleEmailFields);
2883 });
2884
2885
2886
2887 //Handle role restriction changes
2888 jQuery(document).ready(function($) {
2889 //Handle role restriction changes for both data sources
2890 $(document).on('change', '.mxchat-role-select', function() {
2891 const $select = $(this);
2892 const entryId = $select.data('entry-id');
2893 const dataSource = $select.data('data-source');
2894 const roleRestriction = $select.val();
2895 const nonce = $select.data('nonce');
2896
2897 // Visual feedback
2898 $select.prop('disabled', true).addClass('updating');
2899
2900 $.ajax({
2901 url: ajaxurl,
2902 type: 'POST',
2903 data: {
2904 action: 'mxchat_update_role_restriction',
2905 nonce: nonce,
2906 entry_id: entryId,
2907 data_source: dataSource, // NEW: Include data source
2908 role_restriction: roleRestriction
2909 },
2910 success: function(response) {
2911 if (response.success) {
2912 // Show success feedback
2913 $select.removeClass('updating').addClass('updated');
2914 setTimeout(() => {
2915 $select.removeClass('updated');
2916 }, 2000);
2917 } else {
2918 alert('Failed to update role restriction: ' + response.data);
2919 // Revert selection
2920 $select.val($select.data('original-value'));
2921 }
2922 },
2923 error: function() {
2924 alert('Error updating role restriction');
2925 // Revert selection
2926 $select.val($select.data('original-value'));
2927 },
2928 complete: function() {
2929 $select.prop('disabled', false);
2930 }
2931 });
2932
2933 // Store original value for potential revert
2934 $select.data('original-value', roleRestriction);
2935 });
2936
2937 // Store initial values
2938 $('.mxchat-role-select').each(function() {
2939 $(this).data('original-value', $(this).val());
2940 });
2941 });
2942
2943 // Bot Selector Handler
2944 jQuery(document).ready(function($) {
2945 var saveTimer;
2946
2947 // Function to update bot_id in forms
2948 function updateBotIdInForm(formSelector) {
2949 var botId = $('#mxchat-bot-selector').val();
2950 var form = $(formSelector);
2951
2952 if (form.length > 0) {
2953 // Remove existing bot_id hidden input
2954 form.find('input[name="bot_id"]').remove();
2955
2956 // Add new bot_id hidden input if not default
2957 if (botId && botId !== 'default') {
2958 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2959 }
2960 }
2961 }
2962
2963 $('#mxchat-bot-selector').on('change', function() {
2964 var botId = $(this).val();
2965
2966 // Clear any existing timer
2967 clearTimeout(saveTimer);
2968
2969 // Update all forms with new bot_id when bot selection changes
2970 updateBotIdInForm('#mxchat-url-form');
2971 updateBotIdInForm('#mxchat-content-form');
2972
2973 // Save the selection via AJAX
2974 $.ajax({
2975 url: ajaxurl,
2976 type: 'POST',
2977 data: {
2978 action: 'mxchat_save_selected_bot',
2979 bot_id: botId,
2980 nonce: mxchatAdmin.setting_nonce
2981 },
2982 success: function(response) {
2983 if (response.success) {
2984 // Show saved indicator
2985 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2986
2987 // Reload the page after a short delay to refresh content
2988 saveTimer = setTimeout(function() {
2989 var currentUrl = new URL(window.location.href);
2990 currentUrl.searchParams.set('bot_id', botId);
2991 currentUrl.searchParams.set('page', 'mxchat-prompts');
2992 window.location.href = currentUrl.toString();
2993 }, 500);
2994 }
2995 },
2996 error: function() {
2997 console.error('Failed to save bot selection');
2998 }
2999 });
3000 });
3001
3002 // Initialize forms with current bot_id when the page loads
3003 setTimeout(function() {
3004 updateBotIdInForm('#mxchat-url-form');
3005 updateBotIdInForm('#mxchat-content-form');
3006 }, 100);
3007 });
3008
3009 // API Key Status Indicator for Chat Models
3010 jQuery(document).ready(function($) {
3011 function updateChatModelAPIStatus(apiKeyStatuses) {
3012 var selectedModel = $('#model').val();
3013
3014 // Hide all status messages
3015 $('.mxchat-api-status').hide();
3016
3017 // Return early if no model is selected
3018 if (!selectedModel) {
3019 return;
3020 }
3021
3022 // Map models to providers
3023 var provider = null;
3024
3025 if (selectedModel.startsWith('gpt-')) {
3026 provider = 'openai';
3027 } else if (selectedModel.startsWith('claude-')) {
3028 provider = 'claude';
3029 } else if (selectedModel.startsWith('grok-')) {
3030 provider = 'xai';
3031 } else if (selectedModel.startsWith('deepseek-')) {
3032 provider = 'deepseek';
3033 } else if (selectedModel.startsWith('gemini-')) {
3034 provider = 'gemini';
3035 } else if (selectedModel === 'openrouter') {
3036 provider = 'openrouter';
3037 }
3038
3039 // If we have fresh API key data, update the messages
3040 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3041 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
3042 var hasKey = apiKeyStatuses[provider];
3043
3044 if (hasKey) {
3045 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3046 } else {
3047 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3048 }
3049 }
3050
3051 // Show the appropriate status message
3052 if (provider) {
3053 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
3054 }
3055 }
3056
3057 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
3058 var selectedModel = $('#embedding_model').val();
3059
3060 // Hide all status messages
3061 $('.mxchat-embedding-api-status').hide();
3062
3063 // Return early if no model is selected
3064 if (!selectedModel) {
3065 return;
3066 }
3067
3068 // Map models to providers
3069 var provider = null;
3070
3071 if (selectedModel.startsWith('text-embedding-')) {
3072 provider = 'openai';
3073 } else if (selectedModel.startsWith('voyage-')) {
3074 provider = 'voyage';
3075 } else if (selectedModel.startsWith('gemini-embedding-')) {
3076 provider = 'gemini';
3077 }
3078
3079 // If we have fresh API key data, update the messages
3080 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3081 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
3082 var hasKey = apiKeyStatuses[provider];
3083
3084 if (hasKey) {
3085 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3086 } else {
3087 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3088 }
3089 }
3090
3091 // Show the appropriate status message
3092 if (provider) {
3093 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
3094 }
3095 }
3096
3097 function getProviderName(provider) {
3098 var names = {
3099 'openai': 'OpenAI',
3100 'claude': 'Anthropic (Claude)',
3101 'xai': 'X.AI (Grok)',
3102 'deepseek': 'DeepSeek',
3103 'gemini': 'Google Gemini',
3104 'openrouter': 'OpenRouter',
3105 'voyage': 'Voyage AI'
3106 };
3107 return names[provider] || provider;
3108 }
3109
3110 function refreshAPIKeyStatus() {
3111 $.ajax({
3112 url: ajaxurl,
3113 type: 'POST',
3114 data: {
3115 action: 'mxchat_check_api_keys',
3116 nonce: mxchatAdmin.setting_nonce
3117 },
3118 success: function(response) {
3119 if (response.success && response.data) {
3120 updateChatModelAPIStatus(response.data);
3121 updateEmbeddingModelAPIStatus(response.data);
3122 }
3123 }
3124 });
3125 }
3126
3127 // Expose refresh function globally so auto-save can call it
3128 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
3129
3130 // Run on page load
3131 updateChatModelAPIStatus();
3132 updateEmbeddingModelAPIStatus();
3133
3134 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
3135 var urlParams = new URLSearchParams(window.location.search);
3136 if (urlParams.get('settings-updated') === 'true') {
3137 // Page was just reloaded after save, fetch fresh API key status
3138 refreshAPIKeyStatus();
3139 }
3140
3141 // Run when model changes
3142 $('#model').on('change', function() {
3143 updateChatModelAPIStatus();
3144 updateWebSearchToggleVisibility();
3145 });
3146 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
3147
3148 // Web Search toggle visibility based on model
3149 function updateWebSearchToggleVisibility() {
3150 var selectedModel = $('#model').val();
3151 var $wrapper = $('#web-search-toggle-wrapper');
3152 var $unavailableMessage = $('#web-search-unavailable-message');
3153
3154 if (!$wrapper.length) return; // Element doesn't exist
3155
3156 // Get the list of web-search-capable models from data attributes (OpenAI + Gemini)
3157 var openaiModelsAttr = $wrapper.data('openai-models');
3158 var geminiModelsAttr = $wrapper.data('gemini-models');
3159 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
3160
3161 var openaiModels = openaiModelsAttr ? String(openaiModelsAttr).split(',') : [];
3162 var geminiModels = geminiModelsAttr ? String(geminiModelsAttr).split(',') : [];
3163 var unsupportedModels = unsupportedModelsAttr ? String(unsupportedModelsAttr).split(',') : [];
3164
3165 // Check if selected model is an OpenAI or Gemini model that supports web search
3166 var isCapable = openaiModels.includes(selectedModel) || geminiModels.includes(selectedModel);
3167 var isSupported = isCapable && !unsupportedModels.includes(selectedModel);
3168
3169 if (isSupported) {
3170 $wrapper.show();
3171 $unavailableMessage.hide();
3172 } else {
3173 $wrapper.hide();
3174 $unavailableMessage.show();
3175 }
3176 }
3177
3178 // Run on page load
3179 updateWebSearchToggleVisibility();
3180 });
3181
3182 // Transcripts Metrics Dashboard
3183 jQuery(document).ready(function($) {
3184 // Tab switching functionality
3185 $('.mxchat-metrics-tab').on('click', function() {
3186 const tabName = $(this).data('tab');
3187
3188 // Update tab buttons
3189 $('.mxchat-metrics-tab').removeClass('active');
3190 $(this).addClass('active');
3191
3192 // Update panels
3193 $('.mxchat-metrics-panel').removeClass('active');
3194 $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3195
3196 // Initialize chart if activity tab is shown
3197 if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3198 // Small delay to ensure the canvas is visible
3199 setTimeout(function() {
3200 initActivityChart();
3201 }, 50);
3202 }
3203 });
3204
3205 // Initialize chart function
3206 function initActivityChart() {
3207 const canvas = document.getElementById('mxchat-activity-chart');
3208 if (!canvas) return;
3209
3210 // Check if chart already exists and destroy it
3211 if (canvas.chartInstance) {
3212 canvas.chartInstance.destroy();
3213 }
3214
3215 const ctx = canvas.getContext('2d');
3216
3217 // Create gradient for chats line
3218 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3219 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3220 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3221
3222 // Create gradient for messages line
3223 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3224 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3225 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3226
3227 // Simple chart without external library
3228 canvas.chartInstance = new SimpleChart(canvas, {
3229 labels: mxchatChartData.labels,
3230 datasets: [
3231 {
3232 label: 'Chats',
3233 data: mxchatChartData.chats,
3234 borderColor: '#667eea',
3235 backgroundColor: chatsGradient,
3236 fill: true
3237 },
3238 {
3239 label: 'Messages',
3240 data: mxchatChartData.messages,
3241 borderColor: '#764ba2',
3242 backgroundColor: messagesGradient,
3243 fill: true
3244 }
3245 ]
3246 });
3247 }
3248
3249 // Simple chart implementation (no external dependencies)
3250 class SimpleChart {
3251 constructor(canvas, config) {
3252 this.canvas = canvas;
3253 this.ctx = canvas.getContext('2d');
3254 this.config = config;
3255 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3256 this.render();
3257 }
3258
3259 destroy() {
3260 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3261 }
3262
3263 render() {
3264 const dpr = window.devicePixelRatio || 1;
3265 const rect = this.canvas.getBoundingClientRect();
3266
3267 this.canvas.width = rect.width * dpr;
3268 this.canvas.height = rect.height * dpr;
3269 this.ctx.scale(dpr, dpr);
3270
3271 this.canvas.style.width = rect.width + 'px';
3272 this.canvas.style.height = rect.height + 'px';
3273
3274 const width = rect.width - this.padding.left - this.padding.right;
3275 const height = rect.height - this.padding.top - this.padding.bottom;
3276
3277 // Find max value
3278 let maxValue = 0;
3279 this.config.datasets.forEach(dataset => {
3280 const max = Math.max(...dataset.data);
3281 if (max > maxValue) maxValue = max;
3282 });
3283
3284 // Add some padding to max value
3285 maxValue = Math.ceil(maxValue * 1.1);
3286 if (maxValue === 0) maxValue = 10;
3287
3288 // Draw grid lines
3289 this.ctx.strokeStyle = '#e5e7eb';
3290 this.ctx.lineWidth = 1;
3291 const gridLines = 5;
3292
3293 for (let i = 0; i <= gridLines; i++) {
3294 const y = this.padding.top + (height / gridLines) * i;
3295 this.ctx.beginPath();
3296 this.ctx.moveTo(this.padding.left, y);
3297 this.ctx.lineTo(this.padding.left + width, y);
3298 this.ctx.stroke();
3299
3300 // Draw y-axis labels
3301 const value = maxValue - (maxValue / gridLines) * i;
3302 this.ctx.fillStyle = '#6b7280';
3303 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3304 this.ctx.textAlign = 'right';
3305 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3306 }
3307
3308 // Draw datasets
3309 this.config.datasets.forEach(dataset => {
3310 const points = [];
3311 const xStep = width / (this.config.labels.length - 1 || 1);
3312
3313 dataset.data.forEach((value, index) => {
3314 const x = this.padding.left + (xStep * index);
3315 const y = this.padding.top + height - (value / maxValue * height);
3316 points.push({ x, y, value });
3317 });
3318
3319 // Draw filled area
3320 if (dataset.fill && dataset.backgroundColor) {
3321 this.ctx.fillStyle = dataset.backgroundColor;
3322 this.ctx.beginPath();
3323 this.ctx.moveTo(points[0].x, this.padding.top + height);
3324 points.forEach(point => {
3325 this.ctx.lineTo(point.x, point.y);
3326 });
3327 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3328 this.ctx.closePath();
3329 this.ctx.fill();
3330 }
3331
3332 // Draw line
3333 this.ctx.strokeStyle = dataset.borderColor;
3334 this.ctx.lineWidth = 3;
3335 this.ctx.lineCap = 'round';
3336 this.ctx.lineJoin = 'round';
3337
3338 this.ctx.beginPath();
3339 points.forEach((point, index) => {
3340 if (index === 0) {
3341 this.ctx.moveTo(point.x, point.y);
3342 } else {
3343 this.ctx.lineTo(point.x, point.y);
3344 }
3345 });
3346 this.ctx.stroke();
3347
3348 // Draw points
3349 points.forEach(point => {
3350 this.ctx.fillStyle = '#ffffff';
3351 this.ctx.beginPath();
3352 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3353 this.ctx.fill();
3354 this.ctx.strokeStyle = dataset.borderColor;
3355 this.ctx.lineWidth = 2;
3356 this.ctx.stroke();
3357 });
3358 });
3359
3360 // Draw x-axis labels
3361 const xStep = width / (this.config.labels.length - 1 || 1);
3362 this.ctx.fillStyle = '#6b7280';
3363 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3364 this.ctx.textAlign = 'center';
3365
3366 this.config.labels.forEach((label, index) => {
3367 const x = this.padding.left + (xStep * index);
3368 this.ctx.fillText(label, x, this.padding.top + height + 20);
3369 });
3370
3371 // Draw legend
3372 let legendX = this.padding.left;
3373 const legendY = rect.height - 10;
3374
3375 this.config.datasets.forEach((dataset, index) => {
3376 // Color box
3377 this.ctx.fillStyle = dataset.borderColor;
3378 this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3379
3380 // Label
3381 this.ctx.fillStyle = '#374151';
3382 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3383 this.ctx.textAlign = 'left';
3384 this.ctx.fillText(dataset.label, legendX + 18, legendY);
3385
3386 legendX += this.ctx.measureText(dataset.label).width + 40;
3387 });
3388 }
3389 }
3390
3391 // Initialize chart on page load if we're on the activity tab
3392 if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3393 setTimeout(function() {
3394 initActivityChart();
3395 }, 100);
3396 }
3397
3398 // ========================================
3399 // SLACK TEST CONNECTION
3400 // ========================================
3401 $('#mxchat-test-slack-connection').on('click', function() {
3402 var $button = $(this);
3403 var $result = $('#mxchat-slack-test-result');
3404 var originalText = $button.html();
3405
3406 // Show loading state
3407 $button.prop('disabled', true).html(
3408 '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px; animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/></svg>' +
3409 'Testing...'
3410 );
3411
3412 $.ajax({
3413 url: ajaxurl,
3414 type: 'POST',
3415 data: {
3416 action: 'mxchat_test_slack_connection',
3417 nonce: mxchatAdmin.nonce
3418 },
3419 success: function(response) {
3420 $result.show();
3421 if (response.success) {
3422 $result.html(
3423 '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3424 '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3425 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3426 '</div>'
3427 );
3428 } else {
3429 var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3430 var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3431 var textColor = response.data.partial ? '#856404' : '#721c24';
3432 var icon = response.data.partial ? '' : '';
3433 var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3434
3435 $result.html(
3436 '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3437 '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3438 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3439 (response.data.missing_scopes ? '<p style="margin: 12px 0 0; font-size: 13px;">Add these scopes in your Slack app settings and reinstall the app.</p>' : '') +
3440 '</div>'
3441 );
3442 }
3443 },
3444 error: function() {
3445 $result.show().html(
3446 '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3447 '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3448 '</div>'
3449 );
3450 },
3451 complete: function() {
3452 $button.prop('disabled', false).html(originalText);
3453 }
3454 });
3455 });
3456
3457 // Helper function to escape HTML
3458 function escapeHtml(text) {
3459 var div = document.createElement('div');
3460 div.appendChild(document.createTextNode(text));
3461 return div.innerHTML;
3462 }
3463
3464 // ========================================
3465 // DEBUG & OPTIMIZATION TOOLS
3466 // ========================================
3467
3468 // Debug Mode Toggle
3469 $('#mxchat_debug_mode').on('change', function() {
3470 var $toggle = $(this);
3471 var enabled = $toggle.is(':checked') ? 'on' : 'off';
3472 var $label = $toggle.closest('label');
3473
3474 // Add loading indicator next to the toggle label
3475 var $indicator = $label.find('.mxchat-save-indicator');
3476 if ($indicator.length === 0) {
3477 $indicator = $('<span class="mxchat-save-indicator" style="margin-left: 10px;"></span>');
3478 $label.append($indicator);
3479 }
3480 $indicator.html('<span class="mxchat-saving-spinner"></span>').show();
3481
3482 $.ajax({
3483 url: ajaxurl,
3484 type: 'POST',
3485 data: {
3486 action: 'mxchat_toggle_debug_mode',
3487 enabled: enabled,
3488 _ajax_nonce: mxchatAdmin.setting_nonce
3489 },
3490 success: function(response) {
3491 if (response.success) {
3492 // Show success indicator
3493 $indicator.html('<span class="mxchat-save-success">✓</span>');
3494 setTimeout(function() {
3495 $indicator.fadeOut(300);
3496 }, 2000);
3497
3498 // Always refresh the log after toggling
3499 refreshDebugLog();
3500 } else {
3501 $indicator.html('<span class="mxchat-save-error">✗</span>');
3502 alert(response.data.message || 'Error toggling debug mode');
3503 $toggle.prop('checked', !$toggle.is(':checked'));
3504 }
3505 },
3506 error: function() {
3507 $indicator.html('<span class="mxchat-save-error">✗</span>');
3508 alert('Error toggling debug mode');
3509 $toggle.prop('checked', !$toggle.is(':checked'));
3510 }
3511 });
3512 });
3513
3514 // Refresh Debug Log
3515 function refreshDebugLog() {
3516 var $container = $('#mxchat-debug-log');
3517 var $countBadge = $('#mxchat-log-count');
3518
3519 $.ajax({
3520 url: ajaxurl,
3521 type: 'POST',
3522 data: {
3523 action: 'mxchat_get_debug_log',
3524 _ajax_nonce: mxchatAdmin.setting_nonce
3525 },
3526 success: function(response) {
3527 if (response.success) {
3528 var log = response.data.log;
3529 var count = response.data.count;
3530
3531 if (count > 0) {
3532 $countBadge.text(count + ' entries').show();
3533 var html = '<div class="mxchat-debug-log-entries">';
3534 log.forEach(function(entry) {
3535 var typeClass = 'mxchat-log-type-' + entry.type;
3536 var typeLabel = entry.type.replace(/_/g, ' ').toUpperCase();
3537 html += '<div class="mxchat-debug-log-entry ' + typeClass + '">';
3538 html += '<div class="mxchat-log-header">';
3539 html += '<span class="mxchat-log-type">' + escapeHtml(typeLabel) + '</span>';
3540 html += '<span class="mxchat-log-time">' + escapeHtml(entry.time) + '</span>';
3541 html += '</div>';
3542 html += '<div class="mxchat-log-message">' + escapeHtml(entry.message) + '</div>';
3543 if (entry.data) {
3544 html += '<div class="mxchat-log-data">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</div>';
3545 }
3546 html += '</div>';
3547 });
3548 html += '</div>';
3549 $container.html(html);
3550 } else {
3551 $countBadge.hide();
3552 $container.html(
3553 '<div class="mxchat-debug-log-empty">' +
3554 '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="opacity: 0.3;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
3555 '<p>No log entries yet. Enable debug mode to start logging.</p>' +
3556 '</div>'
3557 );
3558 }
3559 }
3560 }
3561 });
3562 }
3563
3564 // Load debug log on page load
3565 if ($('#mxchat-debug-log').length) {
3566 refreshDebugLog();
3567 }
3568
3569 // Refresh Log Button
3570 $('#mxchat-refresh-log').on('click', function() {
3571 var $btn = $(this);
3572 $btn.prop('disabled', true);
3573 refreshDebugLog();
3574 setTimeout(function() {
3575 $btn.prop('disabled', false);
3576 }, 500);
3577 });
3578
3579 // Clear Log Button
3580 $('#mxchat-clear-log').on('click', function() {
3581 if (!confirm('Are you sure you want to clear the debug log?')) {
3582 return;
3583 }
3584
3585 var $btn = $(this);
3586 $btn.prop('disabled', true);
3587
3588 $.ajax({
3589 url: ajaxurl,
3590 type: 'POST',
3591 data: {
3592 action: 'mxchat_clear_debug_log',
3593 _ajax_nonce: mxchatAdmin.setting_nonce
3594 },
3595 success: function(response) {
3596 if (response.success) {
3597 refreshDebugLog();
3598 } else {
3599 alert(response.data.message || 'Error clearing log');
3600 }
3601 },
3602 error: function() {
3603 alert('Error clearing log');
3604 },
3605 complete: function() {
3606 $btn.prop('disabled', false);
3607 }
3608 });
3609 });
3610
3611 // Export Settings Button
3612 $('#mxchat-export-settings').on('click', function() {
3613 var $btn = $(this);
3614 var originalHtml = $btn.html();
3615 $btn.prop('disabled', true).html(
3616 '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/></svg> Exporting...'
3617 );
3618
3619 $.ajax({
3620 url: ajaxurl,
3621 type: 'POST',
3622 data: {
3623 action: 'mxchat_export_settings',
3624 _ajax_nonce: mxchatAdmin.setting_nonce
3625 },
3626 success: function(response) {
3627 if (response.success) {
3628 // Create and download the JSON file
3629 var dataStr = JSON.stringify(response.data.settings, null, 2);
3630 var blob = new Blob([dataStr], { type: 'application/json' });
3631 var url = URL.createObjectURL(blob);
3632 var a = document.createElement('a');
3633 a.href = url;
3634 a.download = response.data.filename;
3635 document.body.appendChild(a);
3636 a.click();
3637 document.body.removeChild(a);
3638 URL.revokeObjectURL(url);
3639 } else {
3640 alert(response.data.message || 'Error exporting settings');
3641 }
3642 },
3643 error: function() {
3644 alert('Error exporting settings');
3645 },
3646 complete: function() {
3647 $btn.prop('disabled', false).html(originalHtml);
3648 }
3649 });
3650 });
3651
3652 // Reset Settings Modal
3653 var $resetModal = $('#mxchat-reset-modal');
3654 var $resetConfirmInput = $('#mxchat-reset-confirmation');
3655 var $resetConfirmBtn = $('#mxchat-reset-confirm');
3656
3657 $('#mxchat-reset-settings').on('click', function() {
3658 $resetModal.fadeIn(200);
3659 $resetConfirmInput.val('').focus();
3660 $resetConfirmBtn.prop('disabled', true);
3661 });
3662
3663 $('#mxchat-reset-modal-close, #mxchat-reset-cancel, .mxch-modal-backdrop').on('click', function() {
3664 $resetModal.fadeOut(200);
3665 });
3666
3667 $resetConfirmInput.on('input', function() {
3668 var value = $(this).val().toUpperCase();
3669 $resetConfirmBtn.prop('disabled', value !== 'RESET');
3670 });
3671
3672 $resetConfirmBtn.on('click', function() {
3673 var $btn = $(this);
3674 var confirmation = $resetConfirmInput.val();
3675
3676 $btn.prop('disabled', true).text('Resetting...');
3677
3678 $.ajax({
3679 url: ajaxurl,
3680 type: 'POST',
3681 data: {
3682 action: 'mxchat_reset_all_settings',
3683 confirmation: confirmation,
3684 _ajax_nonce: mxchatAdmin.setting_nonce
3685 },
3686 success: function(response) {
3687 if (response.success) {
3688 alert(response.data.message);
3689 window.location.reload();
3690 } else {
3691 alert(response.data.message || 'Error resetting settings');
3692 $btn.prop('disabled', false).text('Reset All Settings');
3693 }
3694 },
3695 error: function() {
3696 alert('Error resetting settings');
3697 $btn.prop('disabled', false).text('Reset All Settings');
3698 }
3699 });
3700 });
3701
3702 // Load debug log on page load if we're on the optimization section
3703 if ($('#optimization').hasClass('active') || window.location.hash === '#optimization') {
3704 refreshDebugLog();
3705 }
3706
3707 // Also refresh when switching to optimization tab
3708 $(document).on('click', '[data-section="optimization"]', function() {
3709 setTimeout(refreshDebugLog, 100);
3710 });
3711 });
3712
3713 // Global rate-limit usage: "Reset counter" button (plan-mxchat-20260603-e9b3f9)
3714 jQuery(document).ready(function($) {
3715 var $usage = $('#mxch-global-usage');
3716 if (!$usage.length) {
3717 return;
3718 }
3719
3720 $('#mxch-global-usage-reset').on('click', function() {
3721 var $btn = $(this);
3722 if (!window.confirm('Reset the global usage counter to zero now? This clears how many messages have been used in the current window.')) {
3723 return;
3724 }
3725
3726 var original = $btn.text();
3727 $btn.prop('disabled', true).text('Resetting…');
3728
3729 $.ajax({
3730 url: mxchatAdmin.ajax_url,
3731 type: 'POST',
3732 data: {
3733 action: 'mxchat_reset_global_rate_limit',
3734 bot_id: $usage.data('bot-id') || 'default',
3735 _ajax_nonce: $usage.data('reset-nonce')
3736 },
3737 success: function(response) {
3738 $btn.prop('disabled', false).text(original);
3739 if (response && response.success) {
3740 $('#mxch-global-usage-fill').css('width', (response.data.pct || 0) + '%');
3741 if (response.data.text) {
3742 $('#mxch-global-usage-text').text(response.data.text);
3743 }
3744 } else {
3745 window.alert((response && response.data && response.data.message) || 'Reset failed. Please try again.');
3746 }
3747 },
3748 error: function() {
3749 $btn.prop('disabled', false).text(original);
3750 window.alert('Reset failed. Please try again.');
3751 }
3752 });
3753 });
3754 });