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

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