PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.4
MxChat – AI Chatbot & Content Generation for WordPress v3.0.4
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.0.4, at js/mxchat-admin.js

3,278 lines 152.9 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 // Helper function to open edit modal for intents/actions
15 function mxchatOpenEditModal(intentId, phrases) {
16 const modal = document.getElementById('mxchat-edit-modal');
17 if (!modal) return;
18
19 // Get form fields
20 const intentIdField = document.getElementById('edit_intent_id');
21 const phrasesField = document.getElementById('edit_phrases');
22
23 // Set values
24 intentIdField.value = intentId;
25 phrasesField.value = phrases;
26
27 // Show modal with animation
28 modal.style.display = 'flex';
29 requestAnimationFrame(() => {
30 modal.classList.add('active');
31 });
32
33 // Set up close handlers
34 const closeModal = () => {
35 modal.classList.remove('active');
36 setTimeout(() => {
37 modal.style.display = 'none';
38 }, 300); // Match the CSS transition time
39 };
40
41 // Close button handler
42 const closeBtn = modal.querySelector('.mxchat-modal-close');
43 if (closeBtn) {
44 closeBtn.onclick = closeModal;
45 }
46
47 // Cancel button handler
48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
49 if (cancelBtn) {
50 cancelBtn.onclick = closeModal;
51 }
52
53 // Click outside modal to close
54 modal.onclick = (e) => {
55 if (e.target === modal) {
56 closeModal();
57 }
58 };
59
60 // Focus the textarea
61 phrasesField.focus();
62 }
63 // Live Agent Notice Dismissal Function
64 function dismissLiveAgentNotice() {
65 if (typeof jQuery !== 'undefined' && typeof mxchatLiveAgent !== 'undefined') {
66 jQuery.post(mxchatLiveAgent.ajaxurl, {
67 action: 'dismiss_live_agent_notice',
68 nonce: mxchatLiveAgent.nonce
69 }, function(response) {
70 if (response.success) {
71 jQuery('#mxchat-disabled-notice').fadeOut(300);
72 }
73 }).fail(function() {
74 // Fallback: just hide the notice if AJAX fails
75 jQuery('#mxchat-disabled-notice').fadeOut(300);
76 });
77 } else {
78 // Fallback for cases where jQuery or localized data isn't available
79 var notice = document.getElementById('mxchat-disabled-notice');
80 if (notice) {
81 notice.style.display = 'none';
82 }
83 }
84 }
85
86 // Theme Migration Notice Dismissal Function
87 function dismissThemeMigrationNotice() {
88 if (typeof jQuery !== 'undefined' && typeof mxchatThemeMigration !== 'undefined') {
89 jQuery.post(mxchatThemeMigration.ajaxurl, {
90 action: 'dismiss_theme_migration_notice',
91 nonce: mxchatThemeMigration.nonce
92 }, function(response) {
93 if (response.success) {
94 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
95 }
96 }).fail(function() {
97 // Fallback: just hide the notice if AJAX fails
98 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
99 });
100 } else {
101 // Fallback for cases where jQuery or localized data isn't available
102 var notice = document.getElementById('mxchat-theme-migration-notice');
103 if (notice) {
104 notice.style.display = 'none';
105 }
106 }
107 }
108
109 // Updated mxchatOpenActionModal function to integrate with the new selector
110 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
111 const modal = document.getElementById('mxchat-action-modal');
112 if (!modal) return;
113
114 // Get form fields
115 const actionIdField = document.getElementById('edit_action_id');
116 const labelField = document.getElementById('intent_label');
117 const phrasesField = document.getElementById('action_phrases');
118 const formActionType = document.getElementById('form_action_type');
119 const callbackGroup = document.getElementById('callback_selection_group');
120 const callbackSelect = document.getElementById('callback_function');
121 const saveButton = document.getElementById('mxchat-save-action-btn');
122 const nonceContainer = document.getElementById('action-nonce-container');
123 const thresholdSlider = document.getElementById('similarity_threshold');
124 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
125
126 // Set up modal for edit or create
127 if (isEdit) {
128 saveButton.textContent = 'Update Action';
129 formActionType.value = 'mxchat_edit_intent';
130 actionIdField.value = actionId;
131 labelField.value = label;
132 phrasesField.value = phrases;
133 callbackGroup.style.display = 'none'; // Hide callback selection when editing
134 thresholdSlider.value = threshold; // Set the current threshold value
135 thresholdDisplay.textContent = threshold + '%'; // Update display
136
137 // Remove the required attribute when editing
138 callbackSelect.removeAttribute('required');
139
140 // Update the nonce field for editing
141 nonceContainer.innerHTML = ''; // Clear existing nonce
142 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
143 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
144 }
145 } else {
146 saveButton.textContent = 'Save Action';
147 formActionType.value = 'mxchat_add_intent';
148 actionIdField.value = '';
149 labelField.value = '';
150 phrasesField.value = '';
151 callbackGroup.style.display = 'block'; // Show callback selection when creating
152 thresholdSlider.value = 85; // Default value for new actions
153 thresholdDisplay.textContent = '85%'; // Default display
154
155 // Ensure the required attribute is present when adding
156 callbackSelect.setAttribute('required', 'required');
157
158 // Update the nonce field for adding
159 nonceContainer.innerHTML = ''; // Clear existing nonce
160 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
161 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
162 }
163 }
164
165 // Show modal with animation
166 modal.style.display = 'flex';
167 requestAnimationFrame(() => {
168 modal.classList.add('active');
169 });
170
171 // Set up close handlers
172 const closeModal = () => {
173 modal.classList.remove('active');
174 setTimeout(() => {
175 modal.style.display = 'none';
176 }, 300); // Match the CSS transition time
177 };
178
179 // Close button handler
180 const closeBtn = modal.querySelector('.mxchat-modal-close');
181 if (closeBtn) {
182 closeBtn.onclick = closeModal;
183 }
184
185 // Cancel button handler
186 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
187 if (cancelBtn) {
188 cancelBtn.onclick = closeModal;
189 }
190
191 // Click outside modal to close
192 modal.onclick = (e) => {
193 if (e.target === modal) {
194 closeModal();
195 }
196 };
197
198 // Escape key to close modal
199 document.addEventListener('keydown', function(e) {
200 if (e.key === 'Escape' && modal.classList.contains('active')) {
201 closeModal();
202 }
203 }, { once: true });
204
205 // Focus the first field
206 labelField.focus();
207
208 // Dispatch an event for the action type selector to catch
209 const event = new CustomEvent('mxchatModalOpened', {
210 detail: {
211 isEdit: isEdit,
212 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
213 }
214 });
215 document.dispatchEvent(event);
216
217 return closeModal; // Return close function for external use
218 }
219
220 // Initialize event listeners
221 document.addEventListener('DOMContentLoaded', () => {
222 // Set up edit button handlers for intents
223 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
224 button.onclick = () => {
225 const intentId = button.dataset.intentId;
226 const phrases = button.dataset.phrases;
227 mxchatOpenEditModal(intentId, phrases);
228 };
229 });
230
231 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
232 button.onclick = () => {
233 const actionId = button.dataset.actionId;
234 const phrases = button.dataset.phrases;
235 const label = button.dataset.label;
236 const threshold = button.dataset.threshold || 85;
237 const callbackFunction = button.dataset.callbackFunction;
238 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
239
240 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
241 };
242 });
243
244 // Set up add new action buttons (new functionality)
245 const addActionBtn = document.getElementById('mxchat-add-action-btn');
246 if (addActionBtn) {
247 addActionBtn.onclick = () => mxchatOpenActionModal();
248 }
249
250 const createFirstAction = document.getElementById('mxchat-create-first-action');
251 if (createFirstAction) {
252 createFirstAction.onclick = () => mxchatOpenActionModal();
253 }
254
255 // Setup category-specific new action buttons (new functionality)
256 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
257 button.onclick = () => {
258 const category = button.closest('.mxchat-new-action-card').dataset.category;
259 const closeModal = mxchatOpenActionModal();
260
261 // Pre-select the appropriate callback based on category
262 if (category) {
263 const callbackSelect = document.getElementById('callback_function');
264 if (callbackSelect) {
265 setTimeout(() => {
266 // Map categories to default callbacks
267 const categoryToCallback = {
268 'data_collection': 'mxchat_handle_form_collection',
269 'integrations': 'mxchat_handle_slack_message',
270 'custom_actions': 'mxchat_handle_custom_action',
271 'recommendations': 'mxchat_handle_product_recommendations'
272 // Add more mappings as needed
273 };
274
275 if (categoryToCallback[category]) {
276 callbackSelect.value = categoryToCallback[category];
277 }
278 }, 100);
279 }
280 }
281 };
282 });
283
284 // Handle action toggle switches (new functionality)
285 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
286 toggle.onchange = function() {
287 const actionId = this.dataset.actionId;
288 const isEnabled = this.checked;
289
290 // Show loading indicator
291 const loadingEl = document.getElementById('mxchat-action-loading');
292 if (loadingEl) loadingEl.style.display = 'flex';
293
294 // Send AJAX request to update status
295 fetch(ajaxurl, {
296 method: 'POST',
297 headers: {
298 'Content-Type': 'application/x-www-form-urlencoded',
299 },
300 body: new URLSearchParams({
301 action: 'mxchat_toggle_action',
302 intent_id: actionId,
303 enabled: isEnabled ? 1 : 0,
304 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
305 })
306 })
307 .then(response => response.json())
308 .then(data => {
309 if (!data.success) {
310 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
311 this.checked = !isEnabled; // Revert the toggle
312 }
313 })
314 .catch(error => {
315 //console.error('Error:', error);
316 alert('Server error. Please try again.');
317 this.checked = !isEnabled; // Revert the toggle
318 })
319 .finally(() => {
320 if (loadingEl) loadingEl.style.display = 'none';
321 });
322 };
323 });
324
325 // Handle threshold sliders in action cards (new functionality)
326 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
327 slider.oninput = function() {
328 const actionId = this.id.replace('intent_threshold_', '');
329 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
330 };
331 });
332
333 // Handle threshold save buttons in action cards (new functionality)
334 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
335 button.onclick = function(e) {
336 e.preventDefault();
337 const form = this.closest('form');
338 const intentId = form.querySelector('input[name="intent_id"]').value;
339 const threshold = form.querySelector('input[name="intent_threshold"]').value;
340 const nonce = form.querySelector('input[name="_wpnonce"]').value;
341
342 // Show loading indicator
343 const loadingEl = document.getElementById('mxchat-action-loading');
344 if (loadingEl) loadingEl.style.display = 'flex';
345
346 // Send AJAX request
347 fetch(ajaxurl, {
348 method: 'POST',
349 headers: {
350 'Content-Type': 'application/x-www-form-urlencoded',
351 },
352 body: new URLSearchParams({
353 action: 'mxchat_update_intent_threshold',
354 intent_id: intentId,
355 intent_threshold: threshold,
356 _wpnonce: nonce
357 })
358 })
359 .then(response => response.json())
360 .then(data => {
361 if (data.success) {
362 // Visual feedback of success
363 const card = this.closest('.mxchat-action-card');
364 card.style.background = 'rgba(120, 115, 245, 0.1)';
365 setTimeout(() => {
366 card.style.background = 'white';
367 }, 300);
368 } else {
369 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
370 }
371 })
372 .catch(error => {
373 //console.error('Error:', error);
374 alert('Server error. Please try again.');
375 })
376 .finally(() => {
377 if (loadingEl) loadingEl.style.display = 'none';
378 });
379 };
380 });
381 });
382
383 jQuery(document).ready(function($) {
384 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
385 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
386
387 // --- AJAX Auto-Save ---
388 let $autosaveSections = $('.mxchat-autosave-section');
389
390 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
391 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
392 if ($pineconeAutosaveSection.length) {
393 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
394 //console.log('Added Pinecone section to auto-save monitoring');
395 }
396
397 // Track whether fields have been modified by user
398 const userModifiedFields = new Set();
399
400 if ($autosaveSections.length) {
401 // Track user interactions with input fields to determine if changes are user-initiated
402 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
403 const fieldName = $(this).attr('name');
404 if (fieldName) {
405 userModifiedFields.add(fieldName);
406 }
407 });
408
409 // Handle real-time range slider value updates
410 $autosaveSections.find('input[type="range"]').on('input', function() {
411 const value = $(this).val();
412 $('#threshold_value').text(value);
413 });
414
415 // Handle all input changes (including range slider)
416 // Store pending AJAX requests and debounce timers per field to prevent freezing
417 const pendingRequests = {};
418 const fieldDebounceTimers = {};
419
420 // Helper function to save rate limit fields with request tracking
421 function triggerFieldSave($field, name, pendingRequests) {
422 const value = $field.val();
423
424 // Remove any existing feedback containers for this field
425 $field.siblings('.feedback-container').remove();
426 $field.parent().find('.feedback-container').remove();
427
428 // Create feedback container
429 const feedbackContainer = $('<div class="feedback-container"></div>');
430 const spinner = $('<div class="saving-spinner"></div>');
431 const successIcon = $('<div class="success-icon">✔</div>');
432 $field.after(feedbackContainer);
433 feedbackContainer.append(spinner);
434
435 // Rate limits use the main settings action
436 const ajaxAction = 'mxchat_save_setting';
437 const nonce = mxchatAdmin.setting_nonce;
438
439 // Store the AJAX request so it can be aborted if needed
440 pendingRequests[name] = $.ajax({
441 url: mxchatAdmin.ajax_url,
442 type: 'POST',
443 data: {
444 action: ajaxAction,
445 name: name,
446 value: value,
447 _ajax_nonce: nonce
448 },
449 success: function(response) {
450 delete pendingRequests[name];
451 if (response.success) {
452 spinner.fadeOut(200, function() {
453 feedbackContainer.append(successIcon);
454 successIcon.fadeIn(200).delay(800).fadeOut(200, function() {
455 feedbackContainer.remove();
456 });
457 });
458 } else {
459 feedbackContainer.remove();
460 }
461 },
462 error: function(xhr, textStatus, error) {
463 delete pendingRequests[name];
464 // Don't show error for aborted requests
465 if (textStatus !== 'abort') {
466 feedbackContainer.remove();
467 }
468 }
469 });
470 }
471
472 $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model').on('change', function() {
473 const $field = $(this);
474 const name = $field.attr('name');
475
476 // Debounce rate limit fields to prevent UI freezing from rapid changes
477 if (name && name.indexOf('rate_limits') !== -1) {
478 // Clear any pending timer for this field
479 if (fieldDebounceTimers[name]) {
480 clearTimeout(fieldDebounceTimers[name]);
481 }
482 // Abort any pending AJAX request for this field
483 if (pendingRequests[name]) {
484 pendingRequests[name].abort();
485 // Remove any existing feedback containers for this field
486 $field.siblings('.feedback-container').remove();
487 $field.parent().find('.feedback-container').remove();
488 }
489 // Debounce the save operation
490 const fieldRef = $field;
491 fieldDebounceTimers[name] = setTimeout(function() {
492 triggerFieldSave(fieldRef, name, pendingRequests);
493 }, 300);
494 return;
495 }
496
497 // Skip saving for API key fields that haven't been interacted with and are empty
498 const isApiKeyField = name && (
499 name === 'loops_api_key' ||
500 name === 'api_key' ||
501 name === 'xai_api_key' ||
502 name === 'claude_api_key' ||
503 name === 'voyage_api_key' ||
504 name === 'gemini_api_key' ||
505 name === 'deepseek_api_key' ||
506 name.indexOf('_api_key') !== -1
507 );
508
509 // Skip processing if:
510 // 1. It's an API key field
511 // 2. The user hasn't interacted with it
512 // 3. The field is empty
513 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
514 //console.log('Skipping auto-save for untouched API key field:', name);
515 return;
516 }
517
518 let value;
519
520 // Handle different input types
521 if ($field.attr('type') === 'checkbox') {
522 // *** UPDATED: Handle Pinecone checkboxes differently ***
523 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
524 value = $field.is(':checked') ? '1' : '0';
525 } else {
526 value = $field.is(':checked') ? 'on' : 'off';
527 }
528 } else {
529 value = $field.val();
530 }
531
532 // Create feedback container
533 const feedbackContainer = $('<div class="feedback-container"></div>');
534 const spinner = $('<div class="saving-spinner"></div>');
535 const successIcon = $('<div class="success-icon">✔</div>');
536
537 // Position feedback container based on input type
538 if ($field.closest('.toggle-switch').length) {
539 // Try td first (old layout), then mxc-field-control (new card layout), then fallback to after toggle
540 var $container = $field.closest('td');
541 if (!$container.length) {
542 $container = $field.closest('.mxc-field-control');
543 }
544 if ($container.length) {
545 $container.append(feedbackContainer);
546 } else {
547 $field.closest('.toggle-switch').after(feedbackContainer);
548 }
549 } else if ($field.closest('.mxchat-toggle-switch').length) {
550 // Try mxchat-toggle-container first, then parent div, then fallback to after toggle
551 var $toggleContainer = $field.closest('.mxchat-toggle-container');
552 if ($toggleContainer.length) {
553 $toggleContainer.append(feedbackContainer);
554 } else {
555 $field.closest('.mxchat-toggle-switch').after(feedbackContainer);
556 }
557 } else if ($field.closest('.slider-container').length) {
558 $field.closest('.slider-container').after(feedbackContainer);
559 } else {
560 $field.after(feedbackContainer);
561 }
562 feedbackContainer.append(spinner);
563
564 // Determine which AJAX action and nonce to use:
565 var ajaxAction, nonce;
566 // *** UPDATED: Add Pinecone fields, chunking fields, ACF fields, and custom meta to prompts action ***
567 if (name.indexOf('mxchat_prompts_options') !== -1 ||
568 name === 'mxchat_auto_sync_posts' ||
569 name === 'mxchat_auto_sync_pages' ||
570 name.indexOf('mxchat_auto_sync_') === 0 ||
571 name.indexOf('mxchat_pinecone_addon_options') !== -1 ||
572 name.indexOf('mxchat_chunk') === 0 ||
573 name.indexOf('mxchat_acf_field_') === 0 ||
574 name === 'mxchat_custom_meta_whitelist') { // Chunking, ACF field settings, and custom meta
575 ajaxAction = 'mxchat_save_prompts_setting';
576 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
577 } else {
578 // Otherwise, use the existing AJAX action.
579 ajaxAction = 'mxchat_save_setting';
580 nonce = mxchatAdmin.setting_nonce;
581 }
582
583 // *** ADD THIS: Debug logging for Pinecone fields ***
584 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
585 //console.log('Saving Pinecone field:', name, '=', value);
586 }
587
588 // AJAX save request
589 $.ajax({
590 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
591 type: 'POST',
592 data: {
593 action: ajaxAction,
594 name: name,
595 value: value,
596 _ajax_nonce: nonce
597 },
598 success: function(response) {
599 if (response.success) {
600 spinner.fadeOut(200, function() {
601 feedbackContainer.append(successIcon);
602 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
603 feedbackContainer.remove();
604 });
605 });
606
607 // *** ADD THIS: Refresh API key status after saving an API key ***
608 const isApiKeyField = name && (
609 name === 'api_key' ||
610 name === 'xai_api_key' ||
611 name === 'claude_api_key' ||
612 name === 'voyage_api_key' ||
613 name === 'gemini_api_key' ||
614 name === 'deepseek_api_key' ||
615 name === 'openrouter_api_key' ||
616 name.indexOf('_api_key') !== -1
617 );
618
619 if (isApiKeyField && typeof window.mxchatRefreshAPIKeyStatus === 'function') {
620 window.mxchatRefreshAPIKeyStatus();
621 }
622
623 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
624 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
625 //console.log('Pinecone toggle saved successfully, value:', value);
626
627 // The checkbox state is already updated by the user interaction
628 // But let's make sure the UI state matches the saved value
629 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
630 var settingsDiv = $('.mxchat-pinecone-settings');
631
632 // Double-check the UI state matches what was saved
633 if (value === '1' && !$checkbox.is(':checked')) {
634 $checkbox.prop('checked', true);
635 settingsDiv.slideDown(300);
636 } else if (value === '0' && $checkbox.is(':checked')) {
637 $checkbox.prop('checked', false);
638 settingsDiv.slideUp(300);
639 }
640
641 //console.log('Pinecone UI state synchronized');
642
643 // Check if Knowledge Import tab is currently active
644 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
645 // Show a notice that we need to refresh
646 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
647 if ($knowledgeCard.length > 0) {
648 // Add a refresh notice at the top of the knowledge base card
649 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
650 '<p style="margin: 0;">' +
651 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
652 'Database settings have changed. ' +
653 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
654 '</p></div>');
655
656 $knowledgeCard.prepend(refreshNotice);
657 }
658 } else {
659 // If not on import tab, set a flag to refresh when they go there
660 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
661 }
662 }
663
664 // *** ADD THIS: Debug logging for successful saves ***
665 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
666 //console.log('Pinecone field saved successfully:', name, '=', value);
667 }
668
669 // Check if the response contains a "no changes" message and log it
670 if (response.data && response.data.message === 'No changes detected') {
671 //console.log('No changes detected for field:', name);
672 }
673 } else {
674 // Only show alert for actual errors, not for "no changes"
675 let errorMessage = response.data?.message || 'Unknown error';
676
677 // Don't display an alert for "no changes" message
678 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
679 alert('Error saving: ' + errorMessage);
680 } else {
681 // Still provide visual feedback that no changes were needed
682 spinner.fadeOut(200, function() {
683 feedbackContainer.append(successIcon);
684 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
685 feedbackContainer.remove();
686 });
687 });
688 //console.log('No changes detected for field:', name);
689 return;
690 }
691
692 // Only revert checkbox state if it was an actual error
693 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
694 if ($field.attr('type') === 'checkbox') {
695 $field.prop('checked', !$field.is(':checked'));
696 }
697 }
698
699 // Always clean up the feedback container
700 feedbackContainer.remove();
701 }
702 },
703 error: function(xhr, textStatus, error) {
704 //console.error('AJAX Error:', textStatus, error);
705 alert('An error occurred while saving. Please try again.');
706
707 // Revert checkbox state on error
708 if ($field.attr('type') === 'checkbox') {
709 $field.prop('checked', !$field.is(':checked'));
710 }
711
712 feedbackContainer.remove();
713 }
714 });
715 });
716
717 // Initialize color pickers with debouncing
718 $autosaveSections.find('.my-color-field').each(function() {
719 const $colorField = $(this);
720
721 $(this).wpColorPicker({
722 change: useDebounce(function(event, ui) {
723 // Safety check - ensure we have a valid field and value
724 if (!$colorField || !$colorField.val()) {
725 //console.warn('Color picker not ready');
726 return;
727 }
728
729 const name = $colorField.attr('name');
730 const value = $colorField.val();
731
732 if (!name || !value) {
733 //console.warn('Missing required color picker values');
734 return;
735 }
736
737 // Create feedback container
738 const feedbackContainer = $('<div class="feedback-container"></div>');
739 const spinner = $('<div class="saving-spinner"></div>');
740 const successIcon = $('<div class="success-icon">✔</div>');
741
742 // Position feedback container
743 $colorField.closest('.wp-picker-container').after(feedbackContainer);
744 feedbackContainer.append(spinner);
745
746 // Determine which AJAX action and nonce to use:
747 var ajaxAction, nonce;
748 // Use the new AJAX action for submenu fields:
749 if (name.indexOf('mxchat_prompts_options') !== -1 ||
750 name === 'mxchat_auto_sync_posts' ||
751 name === 'mxchat_auto_sync_pages' ||
752 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
753 ajaxAction = 'mxchat_save_prompts_setting';
754 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
755 } else {
756 // Otherwise, use the existing AJAX action.
757 ajaxAction = 'mxchat_save_setting';
758 nonce = mxchatAdmin.setting_nonce;
759 }
760 // AJAX save request
761 $.ajax({
762 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
763 type: 'POST',
764 data: {
765 action: ajaxAction,
766 name: name,
767 value: value,
768 _ajax_nonce: nonce
769 },
770 success: function(response) {
771 if (response.success) {
772 spinner.fadeOut(200, function() {
773 feedbackContainer.append(successIcon);
774 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
775 feedbackContainer.remove();
776 });
777 });
778 } else {
779 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
780 feedbackContainer.remove();
781 }
782 },
783 error: function() {
784 alert('An error occurred while saving.');
785 feedbackContainer.remove();
786 }
787 });
788 }, 500)
789 });
790 });
791
792 }
793
794 // ========================================
795 // POST TYPE VISIBILITY SETTINGS
796 // ========================================
797 (function() {
798 const $appendToBody = $('#append_to_body');
799 const $visibilityOptions = $('#post-type-visibility-options');
800 const $modeRadios = $('input[name="post_type_visibility_mode"]');
801 const $postTypeList = $('#post-type-list');
802 const $postTypeCheckboxes = $('input[name="post_type_visibility_list[]"]');
803
804 // Toggle visibility options based on auto-display toggle
805 $appendToBody.on('change', function() {
806 if ($(this).is(':checked')) {
807 $visibilityOptions.slideDown(200);
808 } else {
809 $visibilityOptions.slideUp(200);
810 }
811 });
812
813 // Toggle post type list based on mode selection
814 $modeRadios.on('change', function() {
815 const mode = $(this).val();
816 if (mode === 'all') {
817 $postTypeList.slideUp(200);
818 } else {
819 $postTypeList.slideDown(200);
820 }
821
822 // Save mode via AJAX
823 savePostTypeVisibility('post_type_visibility_mode', mode);
824 });
825
826 // Save post type list when checkboxes change
827 $postTypeCheckboxes.on('change', function() {
828 // Collect all checked post types
829 const selectedPostTypes = [];
830 $postTypeCheckboxes.filter(':checked').each(function() {
831 selectedPostTypes.push($(this).val());
832 });
833
834 // Save as JSON array
835 savePostTypeVisibility('post_type_visibility_list', JSON.stringify(selectedPostTypes));
836 });
837
838 // Helper function to save post type visibility settings
839 function savePostTypeVisibility(name, value) {
840 if (typeof mxchatAdmin === 'undefined') return;
841
842 // Find the container element for feedback
843 const $container = name === 'post_type_visibility_mode'
844 ? $('.mxchat-visibility-mode')
845 : $postTypeList;
846
847 // Remove any existing feedback
848 $container.find('.mxchat-save-feedback').remove();
849
850 // Create feedback element
851 const $feedback = $('<span class="mxchat-save-feedback" style="margin-left: 10px; font-size: 12px;"></span>');
852 $feedback.text('Saving...').css('color', '#666');
853
854 if (name === 'post_type_visibility_mode') {
855 $container.append($feedback);
856 } else {
857 $container.before($feedback);
858 }
859
860 $.ajax({
861 url: mxchatAdmin.ajax_url,
862 type: 'POST',
863 data: {
864 action: 'mxchat_save_setting',
865 name: name,
866 value: value,
867 _ajax_nonce: mxchatAdmin.setting_nonce
868 },
869 success: function(response) {
870 if (response.success) {
871 $feedback.text('✓ Saved').css('color', '#46b450');
872 setTimeout(function() {
873 $feedback.fadeOut(300, function() {
874 $(this).remove();
875 });
876 }, 1500);
877 } else {
878 $feedback.text('Error saving').css('color', '#dc3232');
879 }
880 },
881 error: function() {
882 $feedback.text('Error saving').css('color', '#dc3232');
883 }
884 });
885 }
886 })();
887
888 // Toggle visibility handlers
889 function toggleVisibility(selector) {
890 $(selector).on('click', function() {
891 var inputField = $(this).prev('input');
892 if (inputField.attr('type') === 'password') {
893 inputField.attr('type', 'text');
894 $(this).text('Hide');
895 } else {
896 inputField.attr('type', 'password');
897 $(this).text('Show');
898 }
899 });
900 }
901
902 // Initialize all toggle visibility buttons
903 [
904 '#toggleApiKeyVisibility',
905 '#toggleWooCommerceSecretVisibility',
906 '#toggleVoyageAPIKeyVisibility',
907 '#toggleLoopsApiKeyVisibility',
908 '#toggleXaiApiKeyVisibility',
909 '#toggleClaudeApiKeyVisibility',
910 '#toggleBraveApiKeyVisibility',
911 '#toggleWebhookUrlVisibility',
912 '#toggleSecretKeyVisibility',
913 '#toggleBotTokenVisibility',
914 '#toggleDeepSeekApiKeyVisibility',
915 '#toggleGeminiApiKeyVisibility',
916 '#toggleOpenRouterApiKeyVisibility'
917 ].forEach(toggleVisibility);
918
919 function setupMxChatModelSelector() {
920 const $modelSelect = $('#model');
921 const $modelSelectorButton = $('<button>', {
922 type: 'button',
923 id: 'mxchat_model_selector_btn',
924 class: 'button-primary mxchat-model-selector-btn',
925 text: 'Select AI Model'
926 });
927
928 // Replace the select dropdown with a button
929 $modelSelect.hide().after($modelSelectorButton);
930
931 // Update button text to show currently selected model
932 function updateButtonText() {
933 const selectedModel = $modelSelect.val();
934
935 // Check if OpenRouter is selected
936 if (selectedModel === 'openrouter') {
937 const openrouterModelId = $('#openrouter_selected_model').val();
938 const openrouterModelName = $('#openrouter_selected_model_name').val();
939
940 if (openrouterModelName && openrouterModelName.trim() !== '') {
941 $modelSelectorButton.text('OpenRouter: ' + openrouterModelName);
942 } else if (openrouterModelId && openrouterModelId.trim() !== '') {
943 $modelSelectorButton.text('OpenRouter: ' + openrouterModelId);
944 } else {
945 $modelSelectorButton.text('OpenRouter - Select Model');
946 }
947 } else {
948 const selectedModelText = $modelSelect.find('option:selected').text();
949 $modelSelectorButton.text(selectedModelText);
950 }
951 }
952
953 // Initialize button text
954 updateButtonText();
955
956 // Create and append modal HTML
957 const modelSelectorModal = `
958 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
959 <div class="mxchat-model-selector-modal-content">
960 <div class="mxchat-model-selector-modal-header">
961 <h3>Select AI Model</h3>
962 <span class="mxchat-model-selector-modal-close">&times;</span>
963 </div>
964 <div class="mxchat-model-selector-modal-body">
965 <div class="mxchat-model-selector-search-container">
966 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
967 </div>
968 <div class="mxchat-model-selector-categories">
969 <button class="mxchat-model-category-btn active" data-category="all">All</button>
970 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
971 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
972 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
973 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
974 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
975 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
976 </div>
977 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
978 </div>
979 <div class="mxchat-model-selector-modal-footer">
980 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
981 </div>
982 </div>
983 </div>
984 `;
985
986 $('body').append(modelSelectorModal);
987
988 // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
989 window.populateModelsGrid = function(filter = '', category = 'all') {
990 const $grid = $('#mxchat_models_grid');
991 $grid.empty();
992
993 const models = {
994 openrouter: [
995 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
996 ],
997 gemini: [
998 { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro', description: 'Most intelligent model - multimodal understanding & agentic' },
999 { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash', description: 'Most balanced - speed, scale & frontier intelligence' },
1000 { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Advanced thinking - code, math, STEM & long context' },
1001 { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Best price-performance with thinking capabilities' },
1002 { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite', description: 'Ultra fast - optimized for cost & high throughput' },
1003 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Deprecated March 2026' },
1004 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Deprecated March 2026' },
1005 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Deprecated September 2025' },
1006 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Deprecated September 2025' },
1007 ],
1008 openai: [
1009 { value: 'gpt-5.2', label: 'GPT-5.2', description: 'Best general-purpose & agentic model with fast responses' },
1010 { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1', description: 'Flagship for coding & agentic tasks with low reasoning (400K context)' },
1011 { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
1012 { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Faster, more cost-efficient for well-defined tasks and precise prompts' },
1013 { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
1014 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
1015 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
1016 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
1017 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
1018 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
1019 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
1020 ],
1021 claude: [
1022 { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', description: 'Best for complex agents and coding' },
1023 { value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', description: 'Exceptional for specialized complex tasks' },
1024 { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', description: 'Fastest and most intelligent Haiku' },
1025 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Most capable Claude model' },
1026 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
1027 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'High intelligence' },
1028 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
1029 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
1030 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
1031 ],
1032 xai: [
1033 { value: 'grok-4-1-fast-reasoning', label: 'Grok 4.1 Fast (Reasoning)', description: '2M context window and reasoning' },
1034 { value: 'grok-4-1-fast-non-reasoning', label: 'Grok 4.1 Fast (Non-Reasoning)', description: '2M context window and faster responses' },
1035 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' },
1036 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
1037 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
1038 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
1039 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
1040 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
1041 ],
1042 deepseek: [
1043 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
1044 ],
1045 };
1046
1047 let allModels = [];
1048 Object.keys(models).forEach(key => {
1049 if (category === 'all' || category === key) {
1050 allModels = allModels.concat(models[key]);
1051 }
1052 });
1053
1054 // Filter by search term if present
1055 if (filter) {
1056 const lowerFilter = filter.toLowerCase();
1057 allModels = allModels.filter(model =>
1058 model.label.toLowerCase().includes(lowerFilter) ||
1059 model.description.toLowerCase().includes(lowerFilter)
1060 );
1061 }
1062
1063 // Create model cards
1064 allModels.forEach(model => {
1065 const isSelected = $modelSelect.val() === model.value;
1066 const $modelCard = $(`
1067 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
1068 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
1069 <div class="mxchat-model-selector-info">
1070 <h4 class="mxchat-model-selector-title">${model.label}</h4>
1071 <p class="mxchat-model-selector-description">${model.description}</p>
1072 </div>
1073 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1074 </div>
1075 `);
1076 $grid.append($modelCard);
1077 });
1078 };
1079
1080 // Helper function to get icon for each model
1081 function getModelIcon(modelValue) {
1082 if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
1083 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>';
1084 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>';
1085 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>';
1086 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>';
1087 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>';
1088 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1089 }
1090
1091 // Event handlers
1092 $modelSelectorButton.on('click', function() {
1093 $('#mxchat_model_selector_modal').show();
1094 window.populateModelsGrid('', 'all');
1095 });
1096
1097 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1098 $('#mxchat_model_selector_modal').hide();
1099 });
1100
1101 $('.mxchat-model-category-btn').on('click', function() {
1102 $('.mxchat-model-category-btn').removeClass('active');
1103 $(this).addClass('active');
1104 const category = $(this).data('category');
1105 const searchTerm = $('#mxchat_model_search_input').val();
1106 window.populateModelsGrid(searchTerm, category);
1107 });
1108
1109 $('#mxchat_model_search_input').on('input', function() {
1110 const searchTerm = $(this).val();
1111 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1112 window.populateModelsGrid(searchTerm, activeCategory);
1113 });
1114
1115 $(document).on('click', '.mxchat-model-selector-card', function() {
1116 const modelValue = $(this).data('value');
1117 const $modelSelect = $('#model');
1118 const $clickedCard = $(this);
1119
1120 // Check if OpenRouter was selected
1121 if (modelValue === 'openrouter') {
1122 // Load OpenRouter models instead of closing
1123 loadOpenRouterModels();
1124 } else {
1125 // Remove selection from all other cards
1126 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1127
1128 // Add selection to clicked card immediately for instant feedback
1129 $clickedCard.addClass('mxchat-model-selected');
1130 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1131 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1132 }
1133
1134 // Brief delay to show the selection, then start saving
1135 setTimeout(function() {
1136 // Normal model selection
1137 $modelSelect.val(modelValue).trigger('change');
1138
1139 // Show loading state on the card
1140 $clickedCard.css('pointer-events', 'none');
1141 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1142 $clickedCard.find('.mxchat-model-selector-title').html(
1143 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1144 );
1145
1146 // Manually save the model via AJAX
1147 jQuery.ajax({
1148 url: mxchatAdmin.ajax_url,
1149 type: 'POST',
1150 data: {
1151 action: 'mxchat_save_setting',
1152 name: 'model',
1153 value: modelValue,
1154 _ajax_nonce: mxchatAdmin.setting_nonce
1155 },
1156 success: function(response) {
1157 if (response.success) {
1158 // Show success state
1159 $clickedCard.find('.mxchat-model-selector-title').html(
1160 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1161 );
1162
1163 // Update button text after successful save
1164 const selectedModelText = $modelSelect.find('option:selected').text();
1165 $('#mxchat_model_selector_btn').text(selectedModelText);
1166
1167 // Close modal after a short delay to show the success message
1168 setTimeout(function() {
1169 $('#mxchat_model_selector_modal').hide();
1170 // Restore original content and remove selection for next time
1171 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1172 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1173 $clickedCard.css('pointer-events', 'auto');
1174 }, 600);
1175 } else {
1176 // Show error state
1177 $clickedCard.find('.mxchat-model-selector-title').html(
1178 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1179 );
1180
1181 setTimeout(function() {
1182 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1183 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1184 $clickedCard.css('pointer-events', 'auto');
1185 }, 1500);
1186 }
1187 },
1188 error: function() {
1189 // Show error state
1190 $clickedCard.find('.mxchat-model-selector-title').html(
1191 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1192 );
1193
1194 setTimeout(function() {
1195 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1196 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1197 $clickedCard.css('pointer-events', 'auto');
1198 }, 1500);
1199 }
1200 });
1201 }, 300); // 300ms delay to show the selection before starting to save
1202 }
1203 });
1204
1205 // Close modal when clicking outside
1206 $(window).on('click', function(event) {
1207 if ($(event.target).is('#mxchat_model_selector_modal')) {
1208 $('#mxchat_model_selector_modal').hide();
1209 }
1210 });
1211 }
1212
1213 function loadOpenRouterModels() {
1214 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1215 const $modal = $('#mxchat_model_selector_modal');
1216 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1217
1218 if (!apiKey || apiKey.trim() === '') {
1219 // Show error message in modal
1220 $modalBody.html(`
1221 <div style="text-align: center; padding: 40px;">
1222 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1223 <h3>OpenRouter API Key Required</h3>
1224 <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>
1225 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1226 </div>
1227 `);
1228
1229 $('#mxchat_back_to_models').on('click', function(e) {
1230 e.preventDefault();
1231 // CHANGE THIS: Instead of reloading, restore the original modal content
1232 $modalBody.html(`
1233 <div class="mxchat-model-selector-search-container">
1234 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1235 </div>
1236 <div class="mxchat-model-selector-categories">
1237 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1238 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1239 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1240 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1241 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1242 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1243 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1244 </div>
1245 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1246 `);
1247
1248 // Re-populate the grid
1249 populateModelsGrid('', 'all');
1250
1251 // Re-bind event handlers
1252 rebindModalEventHandlers();
1253 });
1254 return;
1255 }
1256
1257 // Show loading state
1258 $modalBody.html(`
1259 <div style="text-align: center; padding: 60px 20px;">
1260 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1261 <h3>Loading OpenRouter Models...</h3>
1262 <p>Fetching available models from OpenRouter</p>
1263 </div>
1264 `);
1265
1266 // Fetch models from OpenRouter
1267 jQuery.ajax({
1268 url: mxchatAdmin.ajax_url,
1269 type: 'POST',
1270 data: {
1271 action: 'mxchat_fetch_openrouter_models',
1272 api_key: apiKey,
1273 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1274 },
1275 success: function(response) {
1276 if (response.success && response.data.models) {
1277 displayOpenRouterModels(response.data.models);
1278 } else {
1279 $modalBody.html(`
1280 <div style="text-align: center; padding: 40px;">
1281 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1282 <h3>Error Loading Models</h3>
1283 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1284 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1285 </div>
1286 `);
1287
1288 $('#mxchat_back_to_models').on('click', function(e) {
1289 e.preventDefault();
1290 // CHANGE THIS: Restore original content instead of reloading
1291 restoreOriginalModalContent();
1292 });
1293 }
1294 },
1295 error: function() {
1296 $modalBody.html(`
1297 <div style="text-align: center; padding: 40px;">
1298 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1299 <h3>Connection Error</h3>
1300 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1301 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1302 </div>
1303 `);
1304
1305 $('#mxchat_back_to_models').on('click', function(e) {
1306 e.preventDefault();
1307 // CHANGE THIS: Restore original content instead of reloading
1308 restoreOriginalModalContent();
1309 });
1310 }
1311 });
1312 }
1313 function restoreOriginalModalContent() {
1314 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1315
1316 $modalBody.html(`
1317 <div class="mxchat-model-selector-search-container">
1318 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1319 </div>
1320 <div class="mxchat-model-selector-categories">
1321 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1322 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1323 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1324 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1325 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1326 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1327 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1328 </div>
1329 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1330 `);
1331
1332 // Re-populate the grid
1333 window.populateModelsGrid('', 'all');
1334
1335 // Re-bind event handlers
1336 rebindModalEventHandlers();
1337 }
1338 function rebindModalEventHandlers() {
1339 const $modal = $('#mxchat_model_selector_modal');
1340
1341 // Re-bind category button clicks
1342 $('.mxchat-model-category-btn').off('click').on('click', function() {
1343 $('.mxchat-model-category-btn').removeClass('active');
1344 $(this).addClass('active');
1345 const category = $(this).data('category');
1346 const searchTerm = $('#mxchat_model_search_input').val();
1347 window.populateModelsGrid(searchTerm, category);
1348 });
1349
1350 // Re-bind search input
1351 $('#mxchat_model_search_input').off('input').on('input', function() {
1352 const searchTerm = $(this).val();
1353 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1354 populateModelsGrid(searchTerm, activeCategory);
1355 });
1356 }
1357
1358 function restoreDefaultModalFooter() {
1359 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1360
1361 // Restore default footer buttons
1362 $modalFooter.html(`
1363 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1364 `);
1365
1366 // Re-bind cancel button
1367 $('#mxchat_cancel_model_selection').on('click', function() {
1368 $('#mxchat_model_selector_modal').hide();
1369 });
1370 }
1371
1372 function displayOpenRouterModels(models) {
1373 const $modal = $('#mxchat_model_selector_modal');
1374 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1375 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1376 const currentSelected = $('#openrouter_selected_model').val();
1377
1378 // Variable to store the currently selected model (in the UI, not yet saved)
1379 let pendingSelection = {
1380 modelId: currentSelected || null,
1381 modelName: $('#openrouter_selected_model_name').val() || null
1382 };
1383
1384 // Build new modal content with search and models
1385 const newContent = `
1386 <div class="mxchat-model-selector-search-container">
1387 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1388 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1389 <strong>${models.length} models available</strong> ·
1390 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1391 </p>
1392 </div>
1393 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1394 `;
1395
1396 // Update footer with Save button for OpenRouter
1397 const footerContent = `
1398 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1399 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1400 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1401 </button>
1402 `;
1403
1404 $modalBody.html(newContent);
1405 $modalFooter.html(footerContent);
1406
1407 // Function to render models
1408 function renderOpenRouterModels(filterText = '') {
1409 const $grid = $('#mxchat_openrouter_models_grid');
1410 $grid.empty();
1411
1412 let filteredModels = models;
1413 if (filterText) {
1414 const lowerFilter = filterText.toLowerCase();
1415 filteredModels = models.filter(m =>
1416 m.id.toLowerCase().includes(lowerFilter) ||
1417 m.name.toLowerCase().includes(lowerFilter) ||
1418 (m.description && m.description.toLowerCase().includes(lowerFilter))
1419 );
1420 }
1421
1422 filteredModels.forEach(model => {
1423 const isSelected = pendingSelection.modelId === model.id;
1424 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1425 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1426
1427 const $card = jQuery(`
1428 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1429 <div class="mxchat-model-selector-icon">
1430 ${getOpenRouterIcon(model.id)}
1431 </div>
1432 <div class="mxchat-model-selector-info">
1433 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1434 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1435 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1436 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1437 </div>
1438 </div>
1439 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1440 </div>
1441 `);
1442
1443 $grid.append($card);
1444 });
1445 }
1446
1447 // Helper to get icon
1448 function getOpenRouterIcon(modelId) {
1449 if (modelId.includes('gpt') || modelId.includes('openai')) {
1450 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>';
1451 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1452 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>';
1453 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1454 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>';
1455 }
1456 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1457 }
1458
1459 // Initial render
1460 renderOpenRouterModels();
1461
1462 // Search handler
1463 $('#mxchat_openrouter_search').on('input', function() {
1464 renderOpenRouterModels($(this).val());
1465 });
1466
1467 $('#mxchat_back_to_provider_select').on('click', function(e) {
1468 e.preventDefault();
1469 // Restore footer to default state
1470 restoreDefaultModalFooter();
1471 // Instead of location.reload(), restore original content
1472 restoreOriginalModalContent();
1473 });
1474
1475 // Back to providers footer button
1476 $('#mxchat_back_to_models_footer').on('click', function(e) {
1477 e.preventDefault();
1478 // Restore footer to default state
1479 restoreDefaultModalFooter();
1480 // Restore original content
1481 restoreOriginalModalContent();
1482 });
1483
1484 // Model selection - just highlight, don't save yet
1485 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1486 e.stopPropagation(); // Prevent triggering the regular model card handler
1487
1488 const modelId = $(this).data('model-id');
1489 const modelName = $(this).data('model-name');
1490
1491 // Remove selection from all cards
1492 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1493
1494 // Add selection to clicked card
1495 $(this).addClass('mxchat-model-selected');
1496 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1497 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1498 }
1499
1500 // Update pending selection
1501 pendingSelection.modelId = modelId;
1502 pendingSelection.modelName = modelName;
1503
1504 // Enable the save button
1505 $('#mxchat_save_openrouter_model').prop('disabled', false);
1506 });
1507
1508 // Save button handler
1509 $('#mxchat_save_openrouter_model').on('click', function() {
1510 const $saveButton = $(this);
1511
1512 if (!pendingSelection.modelId) {
1513 return;
1514 }
1515
1516 // Disable button and show loading state
1517 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1518
1519 // First, save that we're using OpenRouter
1520 jQuery.ajax({
1521 url: mxchatAdmin.ajax_url,
1522 type: 'POST',
1523 data: {
1524 action: 'mxchat_save_setting',
1525 name: 'model',
1526 value: 'openrouter',
1527 _ajax_nonce: mxchatAdmin.setting_nonce
1528 },
1529 success: function() {
1530 // After model is set, save the model ID
1531 jQuery.ajax({
1532 url: mxchatAdmin.ajax_url,
1533 type: 'POST',
1534 data: {
1535 action: 'mxchat_save_setting',
1536 name: 'openrouter_selected_model',
1537 value: pendingSelection.modelId,
1538 _ajax_nonce: mxchatAdmin.setting_nonce
1539 },
1540 success: function() {
1541 // Update DOM immediately
1542 $('#openrouter_selected_model').val(pendingSelection.modelId);
1543
1544 // After model ID is saved, save the display name
1545 jQuery.ajax({
1546 url: mxchatAdmin.ajax_url,
1547 type: 'POST',
1548 data: {
1549 action: 'mxchat_save_setting',
1550 name: 'openrouter_selected_model_name',
1551 value: pendingSelection.modelName,
1552 _ajax_nonce: mxchatAdmin.setting_nonce
1553 },
1554 success: function() {
1555 // Update DOM immediately
1556 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1557
1558 // Update button text
1559 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1560
1561 // Update the "Currently using" message
1562 const $currentSelection = $('#openrouter-current-selection');
1563 if ($currentSelection.length) {
1564 $currentSelection.html(
1565 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1566 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1567 ).show();
1568 }
1569
1570 // Show success state briefly
1571 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1572
1573 // Close modal after short delay
1574 setTimeout(function() {
1575 // Restore footer to default state
1576 restoreDefaultModalFooter();
1577 $modal.hide();
1578 }, 800);
1579 },
1580 error: function() {
1581 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1582 alert('Failed to save model name. Please try again.');
1583 }
1584 });
1585 },
1586 error: function() {
1587 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1588 alert('Failed to save model ID. Please try again.');
1589 }
1590 });
1591 },
1592 error: function() {
1593 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1594 alert('Failed to save OpenRouter selection. Please try again.');
1595 }
1596 });
1597 });
1598
1599 }
1600
1601 // Embedding model selector - completely separate from chat model selector
1602 function setupMxChatEmbeddingModelSelector() {
1603 const $embeddingModelSelect = $('#embedding_model');
1604
1605 // Skip if the element doesn't exist on the page
1606 if ($embeddingModelSelect.length === 0) {
1607 return;
1608 }
1609
1610 const $embeddingModelSelectorButton = $('<button>', {
1611 type: 'button',
1612 id: 'mxchat_embedding_model_selector_btn',
1613 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1614 text: 'Select Embedding Model'
1615 });
1616
1617 // Replace the select dropdown with a button
1618 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1619
1620 // Update button text to show currently selected model
1621 function updateButtonText() {
1622 const selectedModel = $embeddingModelSelect.val();
1623 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1624 $embeddingModelSelectorButton.text(selectedModelText);
1625 }
1626
1627 // Initialize button text
1628 updateButtonText();
1629
1630 // Create a unique ID for the modal to avoid conflicts
1631 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1632
1633 // Create and append modal HTML with unique IDs
1634 const embeddingModelSelectorModal = `
1635 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1636 <div class="mxchat-embedding-model-selector-modal-content">
1637 <div class="mxchat-embedding-model-selector-modal-header">
1638 <h3>Select Embedding Model</h3>
1639 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1640 </div>
1641 <div class="mxchat-embedding-model-selector-modal-body">
1642 <div class="mxchat-embedding-model-selector-search-container">
1643 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1644 </div>
1645 <div class="mxchat-embedding-model-selector-categories">
1646 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1647 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1648 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1649 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1650 </div>
1651 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1652 </div>
1653 <div class="mxchat-embedding-model-selector-modal-footer">
1654 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1655 </div>
1656 </div>
1657 </div>
1658 `;
1659
1660 // Use jQuery's append to ensure it doesn't clash with existing modals
1661 $('body').append(embeddingModelSelectorModal);
1662
1663 // Populate models grid
1664 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1665 const $grid = $('#mxchat_embedding_models_grid');
1666 $grid.empty();
1667
1668 // Define embedding models with descriptions and context lengths
1669 const models = {
1670 openai: [
1671 {
1672 value: 'text-embedding-3-small',
1673 label: 'TE3 Small',
1674 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1675 },
1676 {
1677 value: 'text-embedding-ada-002',
1678 label: 'Ada 2',
1679 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1680 },
1681 {
1682 value: 'text-embedding-3-large',
1683 label: 'TE3 Large',
1684 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1685 }
1686 ],
1687 voyage: [
1688 {
1689 value: 'voyage-3-large',
1690 label: 'Voyage-3 Large',
1691 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1692 }
1693 ],
1694 gemini: [
1695 {
1696 value: 'gemini-embedding-exp-03-07',
1697 label: 'Gemini Embedding',
1698 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1699 }
1700 ]
1701 };
1702
1703 let allModels = [];
1704 Object.keys(models).forEach(key => {
1705 if (category === 'all' || category === key) {
1706 allModels = allModels.concat(models[key]);
1707 }
1708 });
1709
1710 // Filter by search term if present
1711 if (filter) {
1712 const lowerFilter = filter.toLowerCase();
1713 allModels = allModels.filter(model =>
1714 model.label.toLowerCase().includes(lowerFilter) ||
1715 model.description.toLowerCase().includes(lowerFilter)
1716 );
1717 }
1718
1719 // Create model cards
1720 allModels.forEach(model => {
1721 const isSelected = $embeddingModelSelect.val() === model.value;
1722 let providerClass = 'mxchat-embedding-model-provider-openai';
1723
1724 if (model.value.startsWith('voyage-')) {
1725 providerClass = 'mxchat-embedding-model-provider-voyage';
1726 } else if (model.value.startsWith('gemini-embedding-')) {
1727 providerClass = 'mxchat-embedding-model-provider-gemini';
1728 }
1729
1730 let iconHTML = '';
1731 if (model.value.startsWith('voyage-')) {
1732 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1733 } else if (model.value.startsWith('gemini-embedding-')) {
1734 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1735 } else {
1736 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>';
1737 }
1738
1739 const $modelCard = $(`
1740 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1741 <div class="mxchat-embedding-model-selector-icon">
1742 ${iconHTML}
1743 </div>
1744 <div class="mxchat-embedding-model-selector-info">
1745 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1746 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1747 </div>
1748 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1749 </div>
1750 `);
1751
1752 $grid.append($modelCard);
1753 });
1754 }
1755
1756 // Event handlers - use namespaced events to avoid conflicts
1757 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1758 e.stopPropagation(); // Prevent event bubbling
1759 $('#' + embeddingModalId).show();
1760 populateEmbeddingModelsGrid('', 'all');
1761 });
1762
1763 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1764 e.stopPropagation(); // Prevent event bubbling
1765 $('#' + embeddingModalId).hide();
1766 });
1767
1768 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1769 e.stopPropagation(); // Prevent event bubbling
1770 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1771 $(this).addClass('active');
1772 const category = $(this).data('category');
1773 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1774 populateEmbeddingModelsGrid(searchTerm, category);
1775 });
1776
1777 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1778 const searchTerm = $(this).val();
1779 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1780 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1781 });
1782
1783 // Use a direct selector to avoid conflicts with other card elements
1784 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1785 e.stopPropagation(); // Prevent event bubbling
1786 const modelValue = $(this).data('value');
1787
1788 // Important: Only update this specific select element
1789 $embeddingModelSelect.val(modelValue);
1790
1791 // Manually trigger change only on this element
1792 const changeEvent = new Event('change', { bubbles: true });
1793 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1794
1795 // Update button text
1796 updateButtonText();
1797
1798 // Hide modal
1799 $('#' + embeddingModalId).hide();
1800 });
1801
1802 // Close modal when clicking outside - use namespaced events
1803 $(window).on('click.embeddingModelSelector', function(event) {
1804 if ($(event.target).is('#' + embeddingModalId)) {
1805 $('#' + embeddingModalId).hide();
1806 }
1807 });
1808 }
1809
1810 // Call this function after the DOM is fully loaded
1811 $(document).ready(function() {
1812 setupMxChatModelSelector();
1813 setupMxChatEmbeddingModelSelector();
1814 });
1815
1816 // Add Intent Form Submission
1817 $('#mxchat-add-intent-form').on('submit', function(event) {
1818 $('#mxchat-intent-loading').show();
1819 $('#mxchat-intent-loading-text').show();
1820 $(this).find('button[type="submit"]').hide();
1821 });
1822
1823 // Inline Edit Functionality
1824 $('.edit-button').on('click', function() {
1825 var row = $(this).closest('tr');
1826
1827 // Clear URL field if it's a manual content URL (mxchat:// protocol)
1828 var urlEdit = row.find('.url-edit');
1829 if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
1830 urlEdit.val('');
1831 }
1832
1833 // Expand the accordion to show the edit textarea (fixes short content editing)
1834 var contentFull = row.find('.mxchat-content-full');
1835 if (contentFull.length && contentFull.is(':hidden')) {
1836 contentFull.show();
1837 row.find('.mxchat-content-preview').hide();
1838 }
1839
1840 row.find('.content-view, .url-view').hide();
1841 row.find('.content-edit, .url-edit').show();
1842 row.find('.edit-button').hide();
1843 row.find('.save-button').show();
1844 });
1845
1846 // Save button handler
1847 // Save button handler
1848 $('.save-button').on('click', function() {
1849 var button = $(this);
1850 var row = button.closest('tr');
1851 var id = button.data('id');
1852 var nonce = button.data('nonce'); // Get nonce from button data attribute
1853 var newContent = row.find('.content-edit').val();
1854 var newUrl = row.find('.url-edit').val();
1855
1856 //console.log('Nonce from button:', nonce); // Debug
1857
1858 button.prop('disabled', true);
1859 button.text('Saving...');
1860
1861 $.ajax({
1862 url: mxchatAdmin.ajax_url,
1863 type: 'POST',
1864 data: {
1865 action: 'mxchat_save_inline_prompt',
1866 id: id,
1867 article_content: newContent,
1868 article_url: newUrl,
1869 _ajax_nonce: nonce // Use nonce from button
1870 },
1871 success: function(response) {
1872 button.prop('disabled', false);
1873 button.text('Save');
1874
1875 if (response.success) {
1876 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1877 if (newUrl) {
1878 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
1879 } else {
1880 row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
1881 }
1882
1883 row.find('.content-edit, .url-edit').hide();
1884 row.find('.content-view, .url-view').show();
1885 row.find('.save-button').hide();
1886 row.find('.edit-button').show();
1887
1888 // Restore accordion state - show preview, hide full content
1889 row.find('.mxchat-content-preview').show();
1890 row.find('.mxchat-content-full').hide();
1891 } else {
1892 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1893 }
1894 },
1895 error: function() {
1896 button.prop('disabled', false);
1897 button.text('Save');
1898 alert('An error occurred while saving.');
1899 }
1900 });
1901 });
1902
1903
1904 // Questions handling
1905 $('.mxchat-add-question').on('click', function () {
1906 const container = $('#mxchat-additional-questions-container');
1907 const questionCount = container.find('.mxchat-question-row').length + 4;
1908 const questionIndex = container.find('.mxchat-question-row').length;
1909
1910 const newQuestion = `
1911 <div class="mxchat-question-row">
1912 <input type="text"
1913 name="additional_popular_questions[]"
1914 placeholder="Enter Additional Popular Question ${questionCount}"
1915 class="regular-text mxchat-question-input"
1916 data-question-index="${questionIndex}" />
1917 <button type="button" class="button mxchat-remove-question"
1918 aria-label="Remove question">Remove</button>
1919 </div>
1920 `;
1921 container.append(newQuestion);
1922 });
1923
1924 $(document).on('click', '.mxchat-remove-question', function () {
1925 $(this).closest('.mxchat-question-row').remove();
1926 saveQuestions();
1927 });
1928
1929 $(document).on('change', '.mxchat-question-input', function() {
1930 saveQuestions();
1931 });
1932
1933 function saveQuestions() {
1934 const questions = [];
1935 $('.mxchat-question-input').each(function() {
1936 const value = $(this).val().trim();
1937 if (value) {
1938 questions.push(value);
1939 }
1940 });
1941
1942 const feedbackContainer = $('<div class="feedback-container"></div>');
1943 const spinner = $('<div class="saving-spinner"></div>');
1944 const successIcon = $('<div class="success-icon">✔</div>');
1945
1946 // Append feedback after the add button
1947 $('.mxchat-add-question').after(feedbackContainer);
1948 feedbackContainer.append(spinner);
1949
1950 // Save via AJAX
1951 $.ajax({
1952 url: mxchatAdmin.ajax_url,
1953 type: 'POST',
1954 data: {
1955 action: 'mxchat_save_setting',
1956 name: 'additional_popular_questions',
1957 value: JSON.stringify(questions),
1958 _ajax_nonce: mxchatAdmin.setting_nonce
1959 },
1960 success: function(response) {
1961 if (response.success) {
1962 spinner.fadeOut(200, function() {
1963 feedbackContainer.append(successIcon);
1964 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1965 feedbackContainer.remove();
1966 });
1967 });
1968 } else {
1969 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1970 feedbackContainer.remove();
1971 }
1972 },
1973 error: function() {
1974 alert('An error occurred while saving questions.');
1975 feedbackContainer.remove();
1976 }
1977 });
1978 }
1979
1980 // Live agent status handler
1981 const statusToggle = document.getElementById('live_agent_status');
1982 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1983 if (statusToggle && statusText) {
1984 statusToggle.addEventListener('change', function() {
1985 // Update display text
1986 statusText.textContent = this.checked ? 'Online' : 'Offline';
1987
1988 // Send the correct on/off value to the server
1989 if (window.mxchatSaveSetting) {
1990 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1991 }
1992 });
1993 }
1994
1995 // Function to adjust the textarea height to content
1996 function adjustTextareaHeight() {
1997 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1998 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1999 }
2000
2001 // Function to reset the textarea height to initial
2002 function resetTextareaHeight() {
2003 this.style.height = ''; // Remove inline height, reverting to CSS default
2004 }
2005
2006 // Target the specific textarea by ID
2007 var $textarea = $('#system_prompt_instructions');
2008
2009 // Bind events
2010 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2011 .on('blur', resetTextareaHeight); // Reset on blur
2012 });
2013
2014
2015
2016 document.addEventListener('DOMContentLoaded', function() {
2017 // Check if we're on the correct page before initializing
2018 const modal = document.getElementById('mxchat-action-modal');
2019
2020 // Only initialize if the modal exists on this page
2021 if (modal) {
2022 //console.log('MXChat Action Modal JS Loaded');
2023
2024 // Initialize the action modal functionality
2025 initStepBasedActionModal();
2026 }
2027
2028 // Function to initialize the step-based action modal
2029 function initStepBasedActionModal() {
2030 // We already checked for modal existence above, so no need to check again
2031
2032 const actionStep1 = document.getElementById('mxchat-action-step-1');
2033 const actionStep2 = document.getElementById('mxchat-action-step-2');
2034 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2035 const searchInput = document.getElementById('action-type-search');
2036 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2037 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2038 const actionForm = document.getElementById('mxchat-action-form');
2039 const callbackInput = document.getElementById('callback_function');
2040 const actionIdField = document.getElementById('edit_action_id');
2041 const labelField = document.getElementById('intent_label');
2042 const phrasesField = document.getElementById('action_phrases');
2043 const formActionType = document.getElementById('form_action_type');
2044 const nonceContainer = document.getElementById('action-nonce-container');
2045 const thresholdSlider = document.getElementById('similarity_threshold');
2046 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2047
2048 // Rest of your initialization code remains the same...
2049
2050 // Log the structure of one action card for debugging
2051 if (actionCards.length > 0) {
2052 //console.log('First action card data attributes:', actionCards[0].dataset);
2053 //console.log('First action card HTML:', actionCards[0].outerHTML);
2054 }
2055
2056 // Add click event listeners to category buttons
2057 categoryButtons.forEach(button => {
2058 button.addEventListener('click', function() {
2059 //console.log('Category button clicked:', this.dataset.category);
2060
2061 // Remove active class from all buttons
2062 categoryButtons.forEach(btn => btn.classList.remove('active'));
2063
2064 // Add active class to clicked button
2065 this.classList.add('active');
2066
2067 // Get selected category
2068 const category = this.dataset.category;
2069
2070 // Filter action cards
2071 filterActionCards(category, searchInput.value);
2072 });
2073 });
2074
2075 // Add search functionality
2076 if (searchInput) {
2077 searchInput.addEventListener('input', function() {
2078 // Get active category
2079 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2080 //console.log('Search input changed, active category:', activeCategory);
2081
2082 // Filter action cards
2083 filterActionCards(activeCategory, this.value);
2084 });
2085 }
2086
2087 // Add click event listeners to action cards
2088 actionCards.forEach(card => {
2089 card.addEventListener('click', function() {
2090 // Get the action data
2091 const isPro = this.dataset.pro === 'true';
2092 const isInstalled = this.dataset.installed === 'true';
2093 const addonName = this.dataset.addon || '';
2094 const actionValue = this.dataset.value;
2095 const actionLabel = this.dataset.label;
2096 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2097 const actionDescription = this.querySelector('p').textContent;
2098
2099 // Pro check using the proper detection method
2100 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
2101 (mxchatAdmin.is_activated === '1' ||
2102 mxchatAdmin.is_activated === 'true' ||
2103 mxchatAdmin.is_activated === true);
2104
2105 // Handle different states
2106 if (isPro && !proIsActivated) {
2107 // Pro feature but no Pro license
2108 showProFeatureNotice();
2109 return;
2110 }
2111
2112 if (addonName && !isInstalled) {
2113 // Add-on required but not installed
2114 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
2115 showAddonRequiredNotice(addonDisplayName);
2116 return;
2117 }
2118
2119 // If we get here, the action is available - proceed as normal
2120 callbackInput.value = actionValue;
2121
2122 // Update the selected action display in step 2
2123 document.getElementById('selected-action-title').textContent = actionLabel;
2124 document.getElementById('selected-action-description').textContent = actionDescription;
2125 document.getElementById('selected-action-icon').innerHTML =
2126 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2127
2128 // Set a default label based on the action type (user can change it)
2129 if (!labelField.value) {
2130 labelField.value = actionLabel;
2131 }
2132
2133 // Move to step 2
2134 actionStep1.classList.remove('active');
2135 actionStep2.classList.add('active');
2136
2137 // Update modal title
2138 });
2139 });
2140
2141 // Back button functionality
2142 if (backToStep1Btn) {
2143 backToStep1Btn.addEventListener('click', function() {
2144 //console.log('Back button clicked');
2145 actionStep2.classList.remove('active');
2146 actionStep1.classList.add('active');
2147 });
2148 }
2149
2150 // Function to filter action cards by category and search term
2151 function filterActionCards(category, searchTerm) {
2152 searchTerm = searchTerm.toLowerCase().trim();
2153 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2154
2155 let visibleCount = 0;
2156
2157 // Show all cards initially with animation
2158 actionCards.forEach((card, index) => {
2159 // Reset animation
2160 card.style.animation = 'none';
2161 // Trigger reflow
2162 void card.offsetWidth;
2163
2164 // Determine if card should be visible based on category and search term
2165 const cardCategory = card.dataset.category || '';
2166 const matchesCategory = category === 'all' || cardCategory === category;
2167
2168 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2169 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2170 const matchesSearch = searchTerm === '' ||
2171 cardTitle.includes(searchTerm) ||
2172 cardDesc.includes(searchTerm);
2173
2174 // Show/hide card with animation
2175 if (matchesCategory && matchesSearch) {
2176 card.style.display = 'flex';
2177 // Staggered animation for cards
2178 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2179 visibleCount++;
2180 } else {
2181 card.style.display = 'none';
2182 }
2183 });
2184
2185 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2186 }
2187
2188 // Function to show notice for Pro features
2189 function showProFeatureNotice() {
2190 //console.log('Showing Pro feature notice');
2191 // Check if we already have a notification container
2192 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2193
2194 if (!noticeContainer) {
2195 // Create the notice container
2196 noticeContainer = document.createElement('div');
2197 noticeContainer.className = 'mxchat-pro-notice';
2198
2199 // Create content
2200 noticeContainer.innerHTML = `
2201 <div class="mxchat-pro-notice-content">
2202 <h3>MxChat Pro Feature</h3>
2203 <p>This action is available in the Pro version only.</p>
2204 <div class="mxchat-pro-notice-buttons">
2205 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2206 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2207 </div>
2208 </div>
2209 `;
2210
2211 // Append to body
2212 document.body.appendChild(noticeContainer);
2213
2214 // Add close functionality
2215 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2216 closeButton.addEventListener('click', function() {
2217 noticeContainer.classList.remove('active');
2218 setTimeout(() => {
2219 noticeContainer.remove();
2220 }, 300);
2221 });
2222
2223 // Click outside to close
2224 noticeContainer.addEventListener('click', function(e) {
2225 if (e.target === noticeContainer) {
2226 closeButton.click();
2227 }
2228 });
2229
2230 // Show with animation
2231 setTimeout(() => {
2232 noticeContainer.classList.add('active');
2233 }, 10);
2234 } else {
2235 // If it already exists, just make it visible again
2236 noticeContainer.classList.add('active');
2237 }
2238 }
2239
2240 // Function to show notice for add-on requirements
2241 function showAddonRequiredNotice(addonName) {
2242 //console.log(`Showing add-on notice for: ${addonName}`);
2243 // Check if we already have a notification container
2244 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2245
2246 if (!noticeContainer) {
2247 // Create the notice container
2248 noticeContainer = document.createElement('div');
2249 noticeContainer.className = 'mxchat-addon-notice';
2250
2251 // Create content
2252 noticeContainer.innerHTML = `
2253 <div class="mxchat-addon-notice-content">
2254 <span class="mxchat-addon-notice-icon">🧩</span>
2255 <h3>Add-on Required</h3>
2256 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2257 <div class="mxchat-addon-notice-buttons">
2258 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2259 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2260 </div>
2261 </div>
2262 `;
2263
2264 // Append to body
2265 document.body.appendChild(noticeContainer);
2266
2267 // Add close functionality
2268 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2269 closeButton.addEventListener('click', function() {
2270 noticeContainer.classList.remove('active');
2271 setTimeout(() => {
2272 noticeContainer.remove();
2273 }, 300);
2274 });
2275
2276 // Click outside to close
2277 noticeContainer.addEventListener('click', function(e) {
2278 if (e.target === noticeContainer) {
2279 closeButton.click();
2280 }
2281 });
2282
2283 // Show with animation
2284 setTimeout(() => {
2285 noticeContainer.classList.add('active');
2286 }, 10);
2287 } else {
2288 // If it already exists, update the content
2289 const addonNameElement = noticeContainer.querySelector('p strong');
2290 if (addonNameElement) {
2291 addonNameElement.textContent = addonName;
2292 }
2293
2294 // Make it visible again
2295 noticeContainer.classList.add('active');
2296 }
2297 }
2298
2299 // Form submission handling
2300 if (actionForm) {
2301 actionForm.addEventListener('submit', function() {
2302 //console.log('Form submitted');
2303 document.getElementById('mxchat-action-loading').style.display = 'flex';
2304 this.querySelector('button[type="submit"]').disabled = true;
2305 });
2306 }
2307 }
2308
2309 // Setup add action buttons (only if we're on the correct page)
2310 if (modal) {
2311 // Update the modal open function to support the step-based flow
2312 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2313 //console.log('Modal opening, edit mode:', isEdit);
2314
2315 // Get form fields
2316 const actionIdField = document.getElementById('edit_action_id');
2317 const labelField = document.getElementById('intent_label');
2318 const phrasesField = document.getElementById('action_phrases');
2319 const formActionType = document.getElementById('form_action_type');
2320 const callbackInput = document.getElementById('callback_function');
2321 const saveButton = document.getElementById('mxchat-save-action-btn');
2322 const nonceContainer = document.getElementById('action-nonce-container');
2323 const thresholdSlider = document.getElementById('similarity_threshold');
2324 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2325 const actionStep1 = document.getElementById('mxchat-action-step-1');
2326 const actionStep2 = document.getElementById('mxchat-action-step-2');
2327 const searchInput = document.getElementById('action-type-search');
2328
2329 // Set up modal for edit or create
2330 if (isEdit) {
2331 saveButton.textContent = 'Update Action';
2332 formActionType.value = 'mxchat_edit_intent';
2333 actionIdField.value = actionId;
2334 labelField.value = label;
2335 phrasesField.value = phrases;
2336 callbackInput.value = callbackFunction;
2337 thresholdSlider.value = threshold; // Set the current threshold value
2338 thresholdDisplay.textContent = threshold + '%'; // Update display
2339
2340 // NEW: Handle bot selection checkboxes for editing
2341 // First uncheck all bot checkboxes
2342 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2343 checkbox.checked = false;
2344 });
2345
2346 // Then check the ones that should be enabled
2347 if (enabledBots) {
2348 let botsArray;
2349 if (typeof enabledBots === 'string') {
2350 try {
2351 botsArray = JSON.parse(enabledBots);
2352 } catch (e) {
2353 console.warn('Failed to parse enabledBots:', enabledBots);
2354 botsArray = ['default']; // fallback
2355 }
2356 } else if (Array.isArray(enabledBots)) {
2357 botsArray = enabledBots;
2358 } else {
2359 botsArray = ['default']; // fallback
2360 }
2361
2362 botsArray.forEach(botId => {
2363 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2364 if (checkbox) {
2365 checkbox.checked = true;
2366 }
2367 });
2368 } else {
2369 // Fallback to default if no bot data
2370 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2371 if (defaultCheckbox) {
2372 defaultCheckbox.checked = true;
2373 }
2374 }
2375
2376 // Update the nonce field for editing
2377 nonceContainer.innerHTML = ''; // Clear existing nonce
2378 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2379 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2380 }
2381
2382 // For editing, go directly to step 2 and update the selected action display
2383 actionStep1.classList.remove('active');
2384 actionStep2.classList.add('active');
2385
2386 // Find the matching action card to get its details
2387 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2388 let foundCard = null;
2389
2390 actionCards.forEach(card => {
2391 if (card.dataset.value === callbackFunction) {
2392 foundCard = card;
2393 }
2394 });
2395
2396 if (foundCard) {
2397 //console.log('Found matching action card for:', callbackFunction);
2398 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2399 const actionIconElement = foundCard.querySelector('.dashicons');
2400 const actionIcon = actionIconElement
2401 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2402 : 'admin-generic';
2403 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2404
2405 document.getElementById('selected-action-title').textContent = actionLabel;
2406 document.getElementById('selected-action-description').textContent = actionDescription;
2407 document.getElementById('selected-action-icon').innerHTML =
2408 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2409 } else {
2410 //console.log('No matching action card found for:', callbackFunction);
2411 // Fallback if we can't find the card
2412 document.getElementById('selected-action-title').textContent = label;
2413 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2414 document.getElementById('selected-action-icon').innerHTML =
2415 `<span class="dashicons dashicons-admin-generic"></span>`;
2416 }
2417 } else {
2418 //console.log('Setting up create mode');
2419 saveButton.textContent = 'Save Action';
2420 formActionType.value = 'mxchat_add_intent';
2421 actionIdField.value = '';
2422 labelField.value = '';
2423 phrasesField.value = '';
2424 callbackInput.value = '';
2425 thresholdSlider.value = 85; // Default value for new actions
2426 thresholdDisplay.textContent = '85%'; // Default display
2427
2428 // For new actions, ensure default is checked and others are unchecked
2429 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2430 checkbox.checked = (checkbox.value === 'default');
2431 });
2432
2433 // Update the nonce field for adding
2434 nonceContainer.innerHTML = ''; // Clear existing nonce
2435 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2436 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2437 }
2438
2439 // For creating new, start at step 1
2440 actionStep1.classList.add('active');
2441 actionStep2.classList.remove('active');
2442 }
2443
2444 // Show modal with animation
2445 modal.style.display = 'flex';
2446 requestAnimationFrame(() => {
2447 modal.classList.add('active');
2448 });
2449
2450 // Set up close handlers
2451 const closeModal = () => {
2452 //console.log('Closing modal');
2453 modal.classList.remove('active');
2454 setTimeout(() => {
2455 modal.style.display = 'none';
2456 }, 300); // Match the CSS transition time
2457 };
2458
2459 // Close button handler
2460 const closeBtn = modal.querySelector('.mxchat-modal-close');
2461 if (closeBtn) {
2462 closeBtn.onclick = closeModal;
2463 }
2464
2465 // Cancel button handler
2466 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2467 if (cancelBtns) {
2468 cancelBtns.forEach(btn => {
2469 btn.onclick = closeModal;
2470 });
2471 }
2472
2473 // Click outside modal to close
2474 modal.onclick = (e) => {
2475 if (e.target === modal) {
2476 closeModal();
2477 }
2478 };
2479
2480 // Escape key to close modal
2481 document.addEventListener('keydown', function(e) {
2482 if (e.key === 'Escape' && modal.classList.contains('active')) {
2483 closeModal();
2484 }
2485 }, { once: true });
2486
2487 // Focus appropriate field based on current step
2488 if (isEdit || actionStep2.classList.contains('active')) {
2489 if (labelField) labelField.focus();
2490 } else {
2491 if (searchInput) searchInput.focus();
2492 }
2493
2494 return closeModal; // Return close function for external use
2495 };
2496 // Setup add action buttons
2497 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2498 if (addActionBtn) {
2499 //console.log('Add action button found');
2500 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2501 }
2502
2503 const createFirstAction = document.getElementById('mxchat-create-first-action');
2504 if (createFirstAction) {
2505 //console.log('Create first action button found');
2506 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2507 }
2508
2509 // Setup edit buttons
2510 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2511 //console.log('Edit buttons found:', editButtons.length);
2512 editButtons.forEach(button => {
2513 button.onclick = () => {
2514 const actionId = button.dataset.actionId;
2515 const phrases = button.dataset.phrases;
2516 const label = button.dataset.label;
2517 const threshold = button.dataset.threshold || 85;
2518 const callbackFunction = button.dataset.callbackFunction;
2519 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2520
2521 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2522 };
2523 });
2524 }
2525 });
2526
2527 jQuery(document).ready(function($) {
2528 // Auto-expand custom post types container if any are checked
2529 // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2530 function autoExpandIfNeeded() {
2531 const $container = $('#mxchat-custom-post-types-container');
2532 const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2533
2534 if ($container.length === 0) return;
2535
2536 const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2537
2538 if (hasCheckedItems) {
2539 $container.show();
2540 const $icon = $toggleBtn.find('span:last-child');
2541 if ($icon.length) {
2542 $icon.text('');
2543 }
2544 }
2545 }
2546
2547 // Run on page load
2548 autoExpandIfNeeded();
2549 });
2550
2551 document.addEventListener('DOMContentLoaded', function() {
2552 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2553 var modal = document.getElementById('mxchatSampleModal');
2554 var modalClose = document.getElementById('mxchatModalClose');
2555 var closeBtn = document.getElementById('mxchatCloseBtn');
2556 var copyBtn = document.getElementById('mxchatCopyBtn');
2557 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2558 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2559
2560 if (!viewSampleBtn || !modal) {
2561 return;
2562 }
2563
2564 // Open modal
2565 viewSampleBtn.addEventListener('click', function(e) {
2566 e.preventDefault();
2567 e.stopPropagation();
2568 modal.classList.add('mxchat-instructions-show');
2569 });
2570
2571 // Close modal function
2572 function closeModal(e) {
2573 if (e) {
2574 e.preventDefault();
2575 e.stopPropagation();
2576 }
2577 modal.classList.remove('mxchat-instructions-show');
2578 }
2579
2580 // Close modal events
2581 if (modalClose) {
2582 modalClose.addEventListener('click', function(e) {
2583 closeModal(e);
2584 });
2585 }
2586
2587 if (closeBtn) {
2588 closeBtn.addEventListener('click', function(e) {
2589 closeModal(e);
2590 });
2591 }
2592
2593 // Close on backdrop click ONLY (not on hover)
2594 modal.addEventListener('click', function(e) {
2595 // Only close if clicking directly on the overlay, not on child elements
2596 if (e.target === modal) {
2597 closeModal(e);
2598 }
2599 });
2600
2601 // Prevent modal content clicks from closing the modal
2602 if (modalContent) {
2603 modalContent.addEventListener('click', function(e) {
2604 e.stopPropagation();
2605 });
2606 }
2607
2608 // Close on escape key
2609 document.addEventListener('keydown', function(e) {
2610 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2611 closeModal();
2612 }
2613 });
2614
2615 // Copy functionality
2616 if (copyBtn && instructionsContent) {
2617 copyBtn.addEventListener('click', function(e) {
2618 e.preventDefault();
2619 e.stopPropagation();
2620
2621 var text = instructionsContent.textContent;
2622
2623 if (navigator.clipboard) {
2624 navigator.clipboard.writeText(text).then(function() {
2625 showCopySuccess();
2626 }).catch(function() {
2627 fallbackCopy(text);
2628 });
2629 } else {
2630 fallbackCopy(text);
2631 }
2632 });
2633 }
2634
2635 function fallbackCopy(text) {
2636 var textArea = document.createElement('textarea');
2637 textArea.value = text;
2638 textArea.style.position = 'fixed';
2639 textArea.style.left = '-999999px';
2640 textArea.style.top = '-999999px';
2641 document.body.appendChild(textArea);
2642 textArea.select();
2643 try {
2644 document.execCommand('copy');
2645 showCopySuccess();
2646 } catch (err) {
2647 console.error('Copy failed');
2648 }
2649 document.body.removeChild(textArea);
2650 }
2651
2652 function showCopySuccess() {
2653 var originalText = copyBtn.innerHTML;
2654 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!';
2655
2656 setTimeout(function() {
2657 copyBtn.innerHTML = originalText;
2658 }, 2000);
2659 }
2660 });
2661
2662 jQuery(document).ready(function($) {
2663 var emailDependentFields = [
2664 '#email_blocker_header_content',
2665 '#email_blocker_button_text',
2666 '#enable_name_field',
2667 '#name_field_placeholder'
2668 ];
2669
2670 // Add visual indicators
2671 emailDependentFields.forEach(function(fieldId) {
2672 var $row = $(fieldId).closest('tr');
2673 $row.addClass('email-dependent-field');
2674
2675 // Add an icon to show it's dependent
2676 var $label = $row.find('th label, th');
2677 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2678 });
2679
2680 // Hide initially with opacity for smoother transition
2681 emailDependentFields.forEach(function(fieldId) {
2682 $(fieldId).closest('tr').hide().css('opacity', '0');
2683 });
2684
2685 function toggleEmailFields() {
2686 var emailEnabled = $('#enable_email_block').is(':checked');
2687
2688 emailDependentFields.forEach(function(fieldId) {
2689 var $row = $(fieldId).closest('tr');
2690 if (emailEnabled) {
2691 $row.slideDown(400).animate({opacity: 1}, 200);
2692 } else {
2693 $row.animate({opacity: 0}, 200).slideUp(400);
2694 }
2695 });
2696 }
2697
2698 toggleEmailFields();
2699 $('#enable_email_block').on('change', toggleEmailFields);
2700 });
2701
2702
2703
2704 //Handle role restriction changes
2705 jQuery(document).ready(function($) {
2706 //Handle role restriction changes for both data sources
2707 $(document).on('change', '.mxchat-role-select', function() {
2708 const $select = $(this);
2709 const entryId = $select.data('entry-id');
2710 const dataSource = $select.data('data-source');
2711 const roleRestriction = $select.val();
2712 const nonce = $select.data('nonce');
2713
2714 // Visual feedback
2715 $select.prop('disabled', true).addClass('updating');
2716
2717 $.ajax({
2718 url: ajaxurl,
2719 type: 'POST',
2720 data: {
2721 action: 'mxchat_update_role_restriction',
2722 nonce: nonce,
2723 entry_id: entryId,
2724 data_source: dataSource, // NEW: Include data source
2725 role_restriction: roleRestriction
2726 },
2727 success: function(response) {
2728 if (response.success) {
2729 // Show success feedback
2730 $select.removeClass('updating').addClass('updated');
2731 setTimeout(() => {
2732 $select.removeClass('updated');
2733 }, 2000);
2734 } else {
2735 alert('Failed to update role restriction: ' + response.data);
2736 // Revert selection
2737 $select.val($select.data('original-value'));
2738 }
2739 },
2740 error: function() {
2741 alert('Error updating role restriction');
2742 // Revert selection
2743 $select.val($select.data('original-value'));
2744 },
2745 complete: function() {
2746 $select.prop('disabled', false);
2747 }
2748 });
2749
2750 // Store original value for potential revert
2751 $select.data('original-value', roleRestriction);
2752 });
2753
2754 // Store initial values
2755 $('.mxchat-role-select').each(function() {
2756 $(this).data('original-value', $(this).val());
2757 });
2758 });
2759
2760 // Bot Selector Handler
2761 jQuery(document).ready(function($) {
2762 var saveTimer;
2763
2764 // Function to update bot_id in forms
2765 function updateBotIdInForm(formSelector) {
2766 var botId = $('#mxchat-bot-selector').val();
2767 var form = $(formSelector);
2768
2769 if (form.length > 0) {
2770 // Remove existing bot_id hidden input
2771 form.find('input[name="bot_id"]').remove();
2772
2773 // Add new bot_id hidden input if not default
2774 if (botId && botId !== 'default') {
2775 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2776 }
2777 }
2778 }
2779
2780 $('#mxchat-bot-selector').on('change', function() {
2781 var botId = $(this).val();
2782
2783 // Clear any existing timer
2784 clearTimeout(saveTimer);
2785
2786 // Update all forms with new bot_id when bot selection changes
2787 updateBotIdInForm('#mxchat-url-form');
2788 updateBotIdInForm('#mxchat-content-form');
2789
2790 // Save the selection via AJAX
2791 $.ajax({
2792 url: ajaxurl,
2793 type: 'POST',
2794 data: {
2795 action: 'mxchat_save_selected_bot',
2796 bot_id: botId,
2797 nonce: mxchatAdmin.setting_nonce
2798 },
2799 success: function(response) {
2800 if (response.success) {
2801 // Show saved indicator
2802 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2803
2804 // Reload the page after a short delay to refresh content
2805 saveTimer = setTimeout(function() {
2806 var currentUrl = new URL(window.location.href);
2807 currentUrl.searchParams.set('bot_id', botId);
2808 currentUrl.searchParams.set('page', 'mxchat-prompts');
2809 window.location.href = currentUrl.toString();
2810 }, 500);
2811 }
2812 },
2813 error: function() {
2814 console.error('Failed to save bot selection');
2815 }
2816 });
2817 });
2818
2819 // Initialize forms with current bot_id when the page loads
2820 setTimeout(function() {
2821 updateBotIdInForm('#mxchat-url-form');
2822 updateBotIdInForm('#mxchat-content-form');
2823 }, 100);
2824 });
2825
2826 // API Key Status Indicator for Chat Models
2827 jQuery(document).ready(function($) {
2828 function updateChatModelAPIStatus(apiKeyStatuses) {
2829 var selectedModel = $('#model').val();
2830
2831 // Hide all status messages
2832 $('.mxchat-api-status').hide();
2833
2834 // Return early if no model is selected
2835 if (!selectedModel) {
2836 return;
2837 }
2838
2839 // Map models to providers
2840 var provider = null;
2841
2842 if (selectedModel.startsWith('gpt-')) {
2843 provider = 'openai';
2844 } else if (selectedModel.startsWith('claude-')) {
2845 provider = 'claude';
2846 } else if (selectedModel.startsWith('grok-')) {
2847 provider = 'xai';
2848 } else if (selectedModel.startsWith('deepseek-')) {
2849 provider = 'deepseek';
2850 } else if (selectedModel.startsWith('gemini-')) {
2851 provider = 'gemini';
2852 } else if (selectedModel === 'openrouter') {
2853 provider = 'openrouter';
2854 }
2855
2856 // If we have fresh API key data, update the messages
2857 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2858 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
2859 var hasKey = apiKeyStatuses[provider];
2860
2861 if (hasKey) {
2862 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2863 } else {
2864 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2865 }
2866 }
2867
2868 // Show the appropriate status message
2869 if (provider) {
2870 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
2871 }
2872 }
2873
2874 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
2875 var selectedModel = $('#embedding_model').val();
2876
2877 // Hide all status messages
2878 $('.mxchat-embedding-api-status').hide();
2879
2880 // Return early if no model is selected
2881 if (!selectedModel) {
2882 return;
2883 }
2884
2885 // Map models to providers
2886 var provider = null;
2887
2888 if (selectedModel.startsWith('text-embedding-')) {
2889 provider = 'openai';
2890 } else if (selectedModel.startsWith('voyage-')) {
2891 provider = 'voyage';
2892 } else if (selectedModel.startsWith('gemini-embedding-')) {
2893 provider = 'gemini';
2894 }
2895
2896 // If we have fresh API key data, update the messages
2897 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2898 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
2899 var hasKey = apiKeyStatuses[provider];
2900
2901 if (hasKey) {
2902 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2903 } else {
2904 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2905 }
2906 }
2907
2908 // Show the appropriate status message
2909 if (provider) {
2910 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
2911 }
2912 }
2913
2914 function getProviderName(provider) {
2915 var names = {
2916 'openai': 'OpenAI',
2917 'claude': 'Anthropic (Claude)',
2918 'xai': 'X.AI (Grok)',
2919 'deepseek': 'DeepSeek',
2920 'gemini': 'Google Gemini',
2921 'openrouter': 'OpenRouter',
2922 'voyage': 'Voyage AI'
2923 };
2924 return names[provider] || provider;
2925 }
2926
2927 function refreshAPIKeyStatus() {
2928 $.ajax({
2929 url: ajaxurl,
2930 type: 'POST',
2931 data: {
2932 action: 'mxchat_check_api_keys',
2933 nonce: mxchatAdmin.setting_nonce
2934 },
2935 success: function(response) {
2936 if (response.success && response.data) {
2937 updateChatModelAPIStatus(response.data);
2938 updateEmbeddingModelAPIStatus(response.data);
2939 }
2940 }
2941 });
2942 }
2943
2944 // Expose refresh function globally so auto-save can call it
2945 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
2946
2947 // Run on page load
2948 updateChatModelAPIStatus();
2949 updateEmbeddingModelAPIStatus();
2950
2951 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
2952 var urlParams = new URLSearchParams(window.location.search);
2953 if (urlParams.get('settings-updated') === 'true') {
2954 // Page was just reloaded after save, fetch fresh API key status
2955 refreshAPIKeyStatus();
2956 }
2957
2958 // Run when model changes
2959 $('#model').on('change', function() {
2960 updateChatModelAPIStatus();
2961 updateWebSearchToggleVisibility();
2962 });
2963 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
2964
2965 // Web Search toggle visibility based on model
2966 function updateWebSearchToggleVisibility() {
2967 var selectedModel = $('#model').val();
2968 var $wrapper = $('#web-search-toggle-wrapper');
2969 var $unavailableMessage = $('#web-search-unavailable-message');
2970
2971 if (!$wrapper.length) return; // Element doesn't exist
2972
2973 // Get the list of supported OpenAI models from data attribute
2974 var openaiModelsAttr = $wrapper.data('openai-models');
2975 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
2976
2977 var openaiModels = openaiModelsAttr ? openaiModelsAttr.split(',') : [];
2978 var unsupportedModels = unsupportedModelsAttr ? unsupportedModelsAttr.split(',') : [];
2979
2980 // Check if selected model is an OpenAI model that supports web search
2981 var isOpenAI = openaiModels.includes(selectedModel);
2982 var isSupported = isOpenAI && !unsupportedModels.includes(selectedModel);
2983
2984 if (isSupported) {
2985 $wrapper.show();
2986 $unavailableMessage.hide();
2987 } else {
2988 $wrapper.hide();
2989 $unavailableMessage.show();
2990 }
2991 }
2992
2993 // Run on page load
2994 updateWebSearchToggleVisibility();
2995 });
2996
2997 // Transcripts Metrics Dashboard
2998 jQuery(document).ready(function($) {
2999 // Tab switching functionality
3000 $('.mxchat-metrics-tab').on('click', function() {
3001 const tabName = $(this).data('tab');
3002
3003 // Update tab buttons
3004 $('.mxchat-metrics-tab').removeClass('active');
3005 $(this).addClass('active');
3006
3007 // Update panels
3008 $('.mxchat-metrics-panel').removeClass('active');
3009 $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3010
3011 // Initialize chart if activity tab is shown
3012 if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3013 // Small delay to ensure the canvas is visible
3014 setTimeout(function() {
3015 initActivityChart();
3016 }, 50);
3017 }
3018 });
3019
3020 // Initialize chart function
3021 function initActivityChart() {
3022 const canvas = document.getElementById('mxchat-activity-chart');
3023 if (!canvas) return;
3024
3025 // Check if chart already exists and destroy it
3026 if (canvas.chartInstance) {
3027 canvas.chartInstance.destroy();
3028 }
3029
3030 const ctx = canvas.getContext('2d');
3031
3032 // Create gradient for chats line
3033 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3034 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3035 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3036
3037 // Create gradient for messages line
3038 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3039 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3040 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3041
3042 // Simple chart without external library
3043 canvas.chartInstance = new SimpleChart(canvas, {
3044 labels: mxchatChartData.labels,
3045 datasets: [
3046 {
3047 label: 'Chats',
3048 data: mxchatChartData.chats,
3049 borderColor: '#667eea',
3050 backgroundColor: chatsGradient,
3051 fill: true
3052 },
3053 {
3054 label: 'Messages',
3055 data: mxchatChartData.messages,
3056 borderColor: '#764ba2',
3057 backgroundColor: messagesGradient,
3058 fill: true
3059 }
3060 ]
3061 });
3062 }
3063
3064 // Simple chart implementation (no external dependencies)
3065 class SimpleChart {
3066 constructor(canvas, config) {
3067 this.canvas = canvas;
3068 this.ctx = canvas.getContext('2d');
3069 this.config = config;
3070 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3071 this.render();
3072 }
3073
3074 destroy() {
3075 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3076 }
3077
3078 render() {
3079 const dpr = window.devicePixelRatio || 1;
3080 const rect = this.canvas.getBoundingClientRect();
3081
3082 this.canvas.width = rect.width * dpr;
3083 this.canvas.height = rect.height * dpr;
3084 this.ctx.scale(dpr, dpr);
3085
3086 this.canvas.style.width = rect.width + 'px';
3087 this.canvas.style.height = rect.height + 'px';
3088
3089 const width = rect.width - this.padding.left - this.padding.right;
3090 const height = rect.height - this.padding.top - this.padding.bottom;
3091
3092 // Find max value
3093 let maxValue = 0;
3094 this.config.datasets.forEach(dataset => {
3095 const max = Math.max(...dataset.data);
3096 if (max > maxValue) maxValue = max;
3097 });
3098
3099 // Add some padding to max value
3100 maxValue = Math.ceil(maxValue * 1.1);
3101 if (maxValue === 0) maxValue = 10;
3102
3103 // Draw grid lines
3104 this.ctx.strokeStyle = '#e5e7eb';
3105 this.ctx.lineWidth = 1;
3106 const gridLines = 5;
3107
3108 for (let i = 0; i <= gridLines; i++) {
3109 const y = this.padding.top + (height / gridLines) * i;
3110 this.ctx.beginPath();
3111 this.ctx.moveTo(this.padding.left, y);
3112 this.ctx.lineTo(this.padding.left + width, y);
3113 this.ctx.stroke();
3114
3115 // Draw y-axis labels
3116 const value = maxValue - (maxValue / gridLines) * i;
3117 this.ctx.fillStyle = '#6b7280';
3118 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3119 this.ctx.textAlign = 'right';
3120 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3121 }
3122
3123 // Draw datasets
3124 this.config.datasets.forEach(dataset => {
3125 const points = [];
3126 const xStep = width / (this.config.labels.length - 1 || 1);
3127
3128 dataset.data.forEach((value, index) => {
3129 const x = this.padding.left + (xStep * index);
3130 const y = this.padding.top + height - (value / maxValue * height);
3131 points.push({ x, y, value });
3132 });
3133
3134 // Draw filled area
3135 if (dataset.fill && dataset.backgroundColor) {
3136 this.ctx.fillStyle = dataset.backgroundColor;
3137 this.ctx.beginPath();
3138 this.ctx.moveTo(points[0].x, this.padding.top + height);
3139 points.forEach(point => {
3140 this.ctx.lineTo(point.x, point.y);
3141 });
3142 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3143 this.ctx.closePath();
3144 this.ctx.fill();
3145 }
3146
3147 // Draw line
3148 this.ctx.strokeStyle = dataset.borderColor;
3149 this.ctx.lineWidth = 3;
3150 this.ctx.lineCap = 'round';
3151 this.ctx.lineJoin = 'round';
3152
3153 this.ctx.beginPath();
3154 points.forEach((point, index) => {
3155 if (index === 0) {
3156 this.ctx.moveTo(point.x, point.y);
3157 } else {
3158 this.ctx.lineTo(point.x, point.y);
3159 }
3160 });
3161 this.ctx.stroke();
3162
3163 // Draw points
3164 points.forEach(point => {
3165 this.ctx.fillStyle = '#ffffff';
3166 this.ctx.beginPath();
3167 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3168 this.ctx.fill();
3169 this.ctx.strokeStyle = dataset.borderColor;
3170 this.ctx.lineWidth = 2;
3171 this.ctx.stroke();
3172 });
3173 });
3174
3175 // Draw x-axis labels
3176 const xStep = width / (this.config.labels.length - 1 || 1);
3177 this.ctx.fillStyle = '#6b7280';
3178 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3179 this.ctx.textAlign = 'center';
3180
3181 this.config.labels.forEach((label, index) => {
3182 const x = this.padding.left + (xStep * index);
3183 this.ctx.fillText(label, x, this.padding.top + height + 20);
3184 });
3185
3186 // Draw legend
3187 let legendX = this.padding.left;
3188 const legendY = rect.height - 10;
3189
3190 this.config.datasets.forEach((dataset, index) => {
3191 // Color box
3192 this.ctx.fillStyle = dataset.borderColor;
3193 this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3194
3195 // Label
3196 this.ctx.fillStyle = '#374151';
3197 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3198 this.ctx.textAlign = 'left';
3199 this.ctx.fillText(dataset.label, legendX + 18, legendY);
3200
3201 legendX += this.ctx.measureText(dataset.label).width + 40;
3202 });
3203 }
3204 }
3205
3206 // Initialize chart on page load if we're on the activity tab
3207 if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3208 setTimeout(function() {
3209 initActivityChart();
3210 }, 100);
3211 }
3212
3213 // ========================================
3214 // SLACK TEST CONNECTION
3215 // ========================================
3216 $('#mxchat-test-slack-connection').on('click', function() {
3217 var $button = $(this);
3218 var $result = $('#mxchat-slack-test-result');
3219 var originalText = $button.html();
3220
3221 // Show loading state
3222 $button.prop('disabled', true).html(
3223 '<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>' +
3224 'Testing...'
3225 );
3226
3227 $.ajax({
3228 url: ajaxurl,
3229 type: 'POST',
3230 data: {
3231 action: 'mxchat_test_slack_connection',
3232 nonce: mxchatAdmin.nonce
3233 },
3234 success: function(response) {
3235 $result.show();
3236 if (response.success) {
3237 $result.html(
3238 '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3239 '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3240 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3241 '</div>'
3242 );
3243 } else {
3244 var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3245 var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3246 var textColor = response.data.partial ? '#856404' : '#721c24';
3247 var icon = response.data.partial ? '' : '';
3248 var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3249
3250 $result.html(
3251 '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3252 '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3253 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3254 (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>' : '') +
3255 '</div>'
3256 );
3257 }
3258 },
3259 error: function() {
3260 $result.show().html(
3261 '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3262 '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3263 '</div>'
3264 );
3265 },
3266 complete: function() {
3267 $button.prop('disabled', false).html(originalText);
3268 }
3269 });
3270 });
3271
3272 // Helper function to escape HTML
3273 function escapeHtml(text) {
3274 var div = document.createElement('div');
3275 div.appendChild(document.createTextNode(text));
3276 return div.innerHTML;
3277 }
3278 });