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

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

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