PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.0.0
MxChat – AI Chatbot & Content Generation for WordPress v2.0.0
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/mxchat-admin.js +221 -3303 3.2.62.0.0 View file →
@@ -10,9 +10,9 @@
10 10 timeout = setTimeout(later, wait);
11 11 };
12 12 }
13 13
14 -// Helper function to open edit modal for intents/actions
14 +// Helper function to open edit modal
15 15 function mxchatOpenEditModal(intentId, phrases) {
16 16 const modal = document.getElementById('mxchat-edit-modal');
17 17 if (!modal) return;
18 18
@@ -59,168 +59,12 @@
59 59
60 60 // Focus the textarea
61 61 phrasesField.focus();
62 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 63
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 64 // Initialize event listeners
221 65 document.addEventListener('DOMContentLoaded', () => {
222 - // Set up edit button handlers for intents
66 + // Set up edit button handlers
223 67 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
224 68 button.onclick = () => {
225 69 const intentId = button.dataset.intentId;
226 70 const phrases = button.dataset.phrases;
@@ -226,159 +70,8 @@
226 70 const phrases = button.dataset.phrases;
227 71 mxchatOpenEditModal(intentId, phrases);
228 72 };
229 73 });
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 74 });
382 75
383 76 jQuery(document).ready(function($) {
384 77 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
@@ -384,71 +77,64 @@
384 77 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
385 78 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
386 79
387 80 // --- 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();
81 + const $autosaveSections = $('.mxchat-autosave-section');
399 82
400 83 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 84 // Handle real-time range slider value updates
410 85 $autosaveSections.find('input[type="range"]').on('input', function() {
411 86 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 - }
87 + $('#threshold_value').text(value);
422 88 });
423 89
424 90 // 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 = {};
91 + $autosaveSections.find('input, textarea, select').on('change', function() {
92 + const $field = $(this);
93 + const name = $field.attr('name');
94 + let value;
428 95
429 - // Helper function to save rate limit fields with request tracking
430 - function triggerFieldSave($field, name, pendingRequests) {
431 - const value = $field.val();
96 + // Handle different input types
97 + if ($field.attr('type') === 'checkbox') {
98 + value = $field.is(':checked') ? 'on' : 'off';
99 + } else {
100 + value = $field.val();
101 + }
432 102
433 - // Remove any existing feedback containers for this field
434 - $field.siblings('.feedback-container').remove();
435 - $field.parent().find('.feedback-container').remove();
436 -
437 103 // Create feedback container
438 104 const feedbackContainer = $('<div class="feedback-container"></div>');
439 105 const spinner = $('<div class="saving-spinner"></div>');
440 106 const successIcon = $('<div class="success-icon">✔</div>');
441 - $field.after(feedbackContainer);
107 +
108 + // Position feedback container based on input type
109 + if ($field.closest('.toggle-switch').length) {
110 + $field.closest('td').append(feedbackContainer);
111 + } else if ($field.closest('.mxchat-toggle-switch').length) {
112 + $field.closest('.mxchat-toggle-container').append(feedbackContainer);
113 + } else if ($field.closest('.slider-container').length) {
114 + $field.closest('.slider-container').after(feedbackContainer);
115 + } else {
116 + $field.after(feedbackContainer);
117 + }
442 118 feedbackContainer.append(spinner);
443 119
444 - // Rate limits use the main settings action
445 - const ajaxAction = 'mxchat_save_setting';
446 - const nonce = mxchatAdmin.setting_nonce;
120 + // Determine which AJAX action and nonce to use:
121 + var ajaxAction, nonce;
122 + // Use the new AJAX action for submenu fields:
123 + if ( name.indexOf('mxchat_prompts_options') !== -1 ||
124 + name === 'mxchat_auto_sync_posts' ||
125 + name === 'mxchat_auto_sync_pages' ) {
126 + ajaxAction = 'mxchat_save_prompts_setting';
127 + nonce = mxchatPromptsAdmin.prompts_setting_nonce;
128 + } else {
129 + // Otherwise, use the existing AJAX action.
130 + ajaxAction = 'mxchat_save_setting';
131 + nonce = mxchatAdmin.setting_nonce;
132 + }
447 133
448 - // Store the AJAX request so it can be aborted if needed
449 - pendingRequests[name] = $.ajax({
450 - url: mxchatAdmin.ajax_url,
134 + // AJAX save request
135 + $.ajax({
136 + url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
451 137 type: 'POST',
452 138 data: {
453 139 action: ajaxAction,
454 140 name: name,
@@ -455,274 +141,32 @@
455 141 value: value,
456 142 _ajax_nonce: nonce
457 143 },
458 144 success: function(response) {
459 - delete pendingRequests[name];
460 145 if (response.success) {
461 146 spinner.fadeOut(200, function() {
462 147 feedbackContainer.append(successIcon);
463 - successIcon.fadeIn(200).delay(800).fadeOut(200, function() {
148 + successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
464 149 feedbackContainer.remove();
465 150 });
466 151 });
467 152 } else {
153 + alert('Error saving: ' + (response.data?.message || 'Unknown error'));
154 + if ($field.attr('type') === 'checkbox') {
155 + $field.prop('checked', !$field.is(':checked'));
156 + }
468 157 feedbackContainer.remove();
469 158 }
470 159 },
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
160 + error: function() {
161 + alert('An error occurred while saving.');
530 162 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 163 $field.prop('checked', !$field.is(':checked'));
705 164 }
165 + feedbackContainer.remove();
706 166 }
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 - });
167 + });
168 + });
725 169
726 170 // Initialize color pickers with debouncing
727 171 $autosaveSections.find('.my-color-field').each(function() {
728 172 const $colorField = $(this);
@@ -730,9 +174,9 @@
730 174 $(this).wpColorPicker({
731 175 change: useDebounce(function(event, ui) {
732 176 // Safety check - ensure we have a valid field and value
733 177 if (!$colorField || !$colorField.val()) {
734 - //console.warn('Color picker not ready');
178 + console.warn('Color picker not ready');
735 179 return;
736 180 }
737 181
738 182 const name = $colorField.attr('name');
@@ -738,9 +182,9 @@
738 182 const name = $colorField.attr('name');
739 183 const value = $colorField.val();
740 184
741 185 if (!name || !value) {
742 - //console.warn('Missing required color picker values');
186 + console.warn('Missing required color picker values');
743 187 return;
744 188 }
745 189
746 190 // Create feedback container
@@ -751,22 +195,20 @@
751 195 // Position feedback container
752 196 $colorField.closest('.wp-picker-container').after(feedbackContainer);
753 197 feedbackContainer.append(spinner);
754 198
755 - // Determine which AJAX action and nonce to use:
199 + // Determine AJAX action and nonce for color fields:
756 200 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
201 + if ( name.indexOf('mxchat_prompts_options') !== -1 ||
202 + name === 'mxchat_auto_sync_posts' ||
203 + name === 'mxchat_auto_sync_pages' ) {
762 204 ajaxAction = 'mxchat_save_prompts_setting';
763 205 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
764 206 } else {
765 - // Otherwise, use the existing AJAX action.
766 207 ajaxAction = 'mxchat_save_setting';
767 208 nonce = mxchatAdmin.setting_nonce;
768 209 }
210 +
769 211 // AJAX save request
770 212 $.ajax({
771 213 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
772 214 type: 'POST',
@@ -797,1141 +239,115 @@
797 239 }, 500)
798 240 });
799 241 });
800 242
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());
243 + // Reinitialize color pickers when switching tabs
244 + $('.mxchat-nav-tab').on('click.mxchat', function() {
245 + setTimeout(function() {
246 + $('.my-color-field:visible').wpColorPicker('close');
247 + }, 100);
841 248 });
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 249 }
895 -})();
896 250
897 -// Toggle visibility handlers
898 -function toggleVisibility(selector) {
899 - $(selector).on('click', function() {
900 - var inputField = $(this).prev('input');
901 - // Check if this is a CSS-masked field (type="text" with mxchat-api-key-field class)
902 - if (inputField.hasClass('mxchat-api-key-field')) {
903 - // Use CSS class toggle for CSS-masked fields
904 - if (inputField.hasClass('mxchat-show-key')) {
905 - inputField.removeClass('mxchat-show-key');
906 - $(this).text('Show');
907 - } else {
908 - inputField.addClass('mxchat-show-key');
909 - $(this).text('Hide');
910 - }
911 - } else {
912 - // Legacy type toggle for password fields
913 - if (inputField.attr('type') === 'password') {
914 - inputField.attr('type', 'text');
915 - $(this).text('Hide');
916 - } else {
917 - inputField.attr('type', 'password');
918 - $(this).text('Show');
919 - }
920 - }
921 - });
922 -}
923 -
924 -// Initialize all toggle visibility buttons
925 -[
926 - '#toggleApiKeyVisibility',
927 - '#toggleWooCommerceSecretVisibility',
928 - '#toggleVoyageAPIKeyVisibility',
929 - '#toggleLoopsApiKeyVisibility',
930 - '#toggleXaiApiKeyVisibility',
931 - '#toggleClaudeApiKeyVisibility',
932 - '#toggleBraveApiKeyVisibility',
933 - '#toggleWebhookUrlVisibility',
934 - '#toggleSecretKeyVisibility',
935 - '#toggleBotTokenVisibility',
936 - '#toggleDeepSeekApiKeyVisibility',
937 - '#toggleGeminiApiKeyVisibility',
938 - '#toggleOpenRouterApiKeyVisibility'
939 -].forEach(toggleVisibility);
940 -
941 -function setupMxChatModelSelector() {
942 - const $modelSelect = $('#model');
943 - const $modelSelectorButton = $('<button>', {
944 - type: 'button',
945 - id: 'mxchat_model_selector_btn',
946 - class: 'button-primary mxchat-model-selector-btn',
947 - text: 'Select AI Model'
948 - });
949 -
950 - // Replace the select dropdown with a button
951 - $modelSelect.hide().after($modelSelectorButton);
952 -
953 - // Update button text to show currently selected model
954 - function updateButtonText() {
955 - const selectedModel = $modelSelect.val();
251 + // Initialize tabs system
252 + function initTabs() {
253 + // Remove any existing handlers first
254 + $('.mxchat-nav-tab').off('click.mxchat');
956 255
957 - // Check if OpenRouter is selected
958 - if (selectedModel === 'openrouter') {
959 - const openrouterModelId = $('#openrouter_selected_model').val();
960 - const openrouterModelName = $('#openrouter_selected_model_name').val();
256 + // Add new click handlers
257 + $('.mxchat-nav-tab').on('click.mxchat', function(e) {
258 + e.preventDefault();
259 + e.stopPropagation();
961 260
962 - if (openrouterModelName && openrouterModelName.trim() !== '') {
963 - $modelSelectorButton.text('OpenRouter: ' + openrouterModelName);
964 - } else if (openrouterModelId && openrouterModelId.trim() !== '') {
965 - $modelSelectorButton.text('OpenRouter: ' + openrouterModelId);
261 + var $this = $(this);
262 +
263 + // Get tab ID - try href first, fallback to data-tab, then to default
264 + var tabId = $this.attr('href');
265 + if (tabId) {
266 + tabId = tabId.replace('#', '');
966 267 } else {
967 - $modelSelectorButton.text('OpenRouter - Select Model');
268 + tabId = $this.data('tab') || 'chatbot';
968 269 }
969 - } else {
970 - const selectedModelText = $modelSelect.find('option:selected').text();
971 - $modelSelectorButton.text(selectedModelText);
972 - }
973 - }
974 -
975 - // Initialize button text
976 - updateButtonText();
977 -
978 - // Create and append modal HTML
979 - const modelSelectorModal = `
980 - <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
981 - <div class="mxchat-model-selector-modal-content">
982 - <div class="mxchat-model-selector-modal-header">
983 - <h3>Select AI Model</h3>
984 - <span class="mxchat-model-selector-modal-close">&times;</span>
985 - </div>
986 - <div class="mxchat-model-selector-modal-body">
987 - <div class="mxchat-model-selector-search-container">
988 - <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
989 - </div>
990 - <div class="mxchat-model-selector-categories">
991 - <button class="mxchat-model-category-btn active" data-category="all">All</button>
992 - <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
993 - <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
994 - <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
995 - <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
996 - <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
997 - <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
998 - <button class="mxchat-model-category-btn" data-category="custom">Custom / Local</button>
999 - </div>
1000 - <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1001 - </div>
1002 - <div class="mxchat-model-selector-modal-footer">
1003 - <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1004 - </div>
1005 - </div>
1006 - </div>
1007 - `;
1008 -
1009 - $('body').append(modelSelectorModal);
1010 -
1011 - // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
1012 - window.populateModelsGrid = function(filter = '', category = 'all') {
1013 - const $grid = $('#mxchat_models_grid');
1014 - $grid.empty();
1015 -
1016 - const models = {
1017 - openrouter: [
1018 - { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
1019 - ],
1020 - gemini: [
1021 - { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro', description: 'Most intelligent model - multimodal understanding & agentic' },
1022 - { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash', description: 'Most balanced - speed, scale & frontier intelligence' },
1023 - { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Advanced thinking - code, math, STEM & long context' },
1024 - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Best price-performance with thinking capabilities' },
1025 - { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite', description: 'Ultra fast - optimized for cost & high throughput' },
1026 - { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Deprecated March 2026' },
1027 - { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Deprecated March 2026' },
1028 - { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Deprecated September 2025' },
1029 - { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Deprecated September 2025' },
1030 - ],
1031 - openai: [
1032 - { value: 'gpt-5.5', label: 'GPT-5.5', description: 'Latest Flagship — newest OpenAI reasoning and coding model' },
1033 - { value: 'gpt-5.4', label: 'GPT-5.4', description: 'Flagship reasoning and coding model' },
1034 - { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', description: 'Fast and affordable with 400K context' },
1035 - { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', description: 'Fastest and cheapest for lightweight tasks' },
1036 - { value: 'gpt-5.3-chat-latest', label: 'GPT-5.3 Chat', description: 'Optimized for natural conversations with reduced hallucinations' },
1037 - { value: 'gpt-5.2', label: 'GPT-5.2', description: 'Best general-purpose & agentic model with fast responses' },
1038 - { value: 'gpt-5.1-chat-latest', label: 'GPT-5.1 Chat Latest', description: 'Recommended for most use cases' },
1039 - { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1', description: 'Flagship for coding & agentic tasks with low reasoning (400K context)' },
1040 - { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
1041 - { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Fast and lightweight' },
1042 - { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
1043 - ],
1044 - claude: [
1045 - { value: 'claude-opus-4-7', label: 'Claude Opus 4.7', description: 'Latest Flagship — newest Anthropic flagship model' },
1046 - { value: 'claude-opus-4-6', label: 'Claude Opus 4.6', description: 'Most capable Claude model - recommended' },
1047 - { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', description: 'Latest Sonnet - excellent balance of speed and capability' },
1048 - { value: 'claude-opus-4-5', label: 'Claude Opus 4.5', description: 'Highly capable for complex tasks' },
1049 - { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', description: 'Best for complex agents and coding' },
1050 - { value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', description: 'Exceptional for specialized complex tasks' },
1051 - { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', description: 'Fastest and most intelligent Haiku' },
1052 - { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Complex tasks' },
1053 - { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
1054 - ],
1055 - xai: [
1056 - { value: 'grok-4-1-fast-reasoning', label: 'Grok 4.1 Fast (Reasoning)', description: '2M context window and reasoning' },
1057 - { value: 'grok-4-1-fast-non-reasoning', label: 'Grok 4.1 Fast (Non-Reasoning)', description: '2M context window and faster responses' },
1058 - { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' },
1059 - { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
1060 - { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
1061 - { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
1062 - { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
1063 - { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
1064 - ],
1065 - deepseek: [
1066 - { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
1067 - ],
1068 - custom: [
1069 - { value: 'custom-provider', label: 'Custom Provider', description: 'OpenAI-compatible local LLM (Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI) — configure Base URL, key, and model in API Keys tab' },
1070 - ],
1071 - };
1072 -
1073 - let allModels = [];
1074 - Object.keys(models).forEach(key => {
1075 - if (category === 'all' || category === key) {
1076 - allModels = allModels.concat(models[key]);
270 +
271 + // Safety check for empty tabId
272 + if (!tabId) {
273 + console.warn('No tab identifier found');
274 + return;
1077 275 }
1078 - });
1079 -
1080 - // Filter by search term if present
1081 - if (filter) {
1082 - const lowerFilter = filter.toLowerCase();
1083 - allModels = allModels.filter(model =>
1084 - model.label.toLowerCase().includes(lowerFilter) ||
1085 - model.description.toLowerCase().includes(lowerFilter)
1086 - );
1087 - }
1088 -
1089 - // Create model cards
1090 - allModels.forEach(model => {
1091 - const isSelected = $modelSelect.val() === model.value;
1092 - const $modelCard = $(`
1093 - <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
1094 - <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
1095 - <div class="mxchat-model-selector-info">
1096 - <h4 class="mxchat-model-selector-title">${model.label}</h4>
1097 - <p class="mxchat-model-selector-description">${model.description}</p>
1098 - </div>
1099 - ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1100 - </div>
1101 - `);
1102 - $grid.append($modelCard);
1103 - });
1104 - };
1105 -
1106 - // Helper function to get icon for each model
1107 - function getModelIcon(modelValue) {
1108 - if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
1109 - 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>';
1110 - 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>';
1111 - 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>';
1112 - 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>';
1113 - if (modelValue === 'custom-provider' || modelValue.startsWith('custom-')) return '<span class="dashicons dashicons-admin-site-alt3" style="font-size: 24px; color: #2c8a3d;"></span>';
1114 - 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>';
1115 - return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1116 - }
1117 -
1118 - // Event handlers
1119 - $modelSelectorButton.on('click', function() {
1120 - $('#mxchat_model_selector_modal').show();
1121 - window.populateModelsGrid('', 'all');
1122 - });
1123 -
1124 - $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1125 - $('#mxchat_model_selector_modal').hide();
1126 - });
1127 -
1128 - $('.mxchat-model-category-btn').on('click', function() {
1129 - $('.mxchat-model-category-btn').removeClass('active');
1130 - $(this).addClass('active');
1131 - const category = $(this).data('category');
1132 - const searchTerm = $('#mxchat_model_search_input').val();
1133 - window.populateModelsGrid(searchTerm, category);
1134 - });
1135 -
1136 - $('#mxchat_model_search_input').on('input', function() {
1137 - const searchTerm = $(this).val();
1138 - const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1139 - window.populateModelsGrid(searchTerm, activeCategory);
1140 - });
1141 -
1142 -$(document).on('click', '.mxchat-model-selector-card', function() {
1143 - const modelValue = $(this).data('value');
1144 - const $modelSelect = $('#model');
1145 - const $clickedCard = $(this);
1146 -
1147 - // Check if OpenRouter was selected
1148 - if (modelValue === 'openrouter') {
1149 - // Load OpenRouter models instead of closing
1150 - loadOpenRouterModels();
1151 - } else {
1152 - // Remove selection from all other cards
1153 - $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1154 -
1155 - // Add selection to clicked card immediately for instant feedback
1156 - $clickedCard.addClass('mxchat-model-selected');
1157 - if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1158 - $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1159 - }
1160 -
1161 - // Brief delay to show the selection, then start saving
1162 - setTimeout(function() {
1163 - // Normal model selection
1164 - $modelSelect.val(modelValue).trigger('change');
1165 -
1166 - // Show loading state on the card
1167 - $clickedCard.css('pointer-events', 'none');
1168 - const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1169 - $clickedCard.find('.mxchat-model-selector-title').html(
1170 - '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1171 - );
1172 -
1173 - // Manually save the model via AJAX
1174 - jQuery.ajax({
1175 - url: mxchatAdmin.ajax_url,
1176 - type: 'POST',
1177 - data: {
1178 - action: 'mxchat_save_setting',
1179 - name: 'model',
1180 - value: modelValue,
1181 - _ajax_nonce: mxchatAdmin.setting_nonce
1182 - },
1183 - success: function(response) {
1184 - if (response.success) {
1185 - // Show success state
1186 - $clickedCard.find('.mxchat-model-selector-title').html(
1187 - '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1188 - );
1189 -
1190 - // Update button text after successful save
1191 - const selectedModelText = $modelSelect.find('option:selected').text();
1192 - $('#mxchat_model_selector_btn').text(selectedModelText);
1193 -
1194 - // Close modal after a short delay to show the success message
1195 - setTimeout(function() {
1196 - $('#mxchat_model_selector_modal').hide();
1197 - // Restore original content and remove selection for next time
1198 - $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1199 - $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1200 - $clickedCard.css('pointer-events', 'auto');
1201 - }, 600);
1202 - } else {
1203 - // Show error state
1204 - $clickedCard.find('.mxchat-model-selector-title').html(
1205 - '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1206 - );
1207 -
1208 - setTimeout(function() {
1209 - $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1210 - $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1211 - $clickedCard.css('pointer-events', 'auto');
1212 - }, 1500);
1213 - }
1214 - },
1215 - error: function() {
1216 - // Show error state
1217 - $clickedCard.find('.mxchat-model-selector-title').html(
1218 - '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1219 - );
1220 -
1221 - setTimeout(function() {
1222 - $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1223 - $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1224 - $clickedCard.css('pointer-events', 'auto');
1225 - }, 1500);
1226 - }
1227 - });
1228 - }, 300); // 300ms delay to show the selection before starting to save
1229 - }
1230 -});
1231 -
1232 - // Close modal when clicking outside
1233 - $(window).on('click', function(event) {
1234 - if ($(event.target).is('#mxchat_model_selector_modal')) {
1235 - $('#mxchat_model_selector_modal').hide();
1236 - }
1237 - });
1238 -}
1239 -
1240 -function loadOpenRouterModels() {
1241 - const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1242 - const $modal = $('#mxchat_model_selector_modal');
1243 - const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1244 -
1245 - if (!apiKey || apiKey.trim() === '') {
1246 - // Show error message in modal
1247 - $modalBody.html(`
1248 - <div style="text-align: center; padding: 40px;">
1249 - <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1250 - <h3>OpenRouter API Key Required</h3>
1251 - <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>
1252 - <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1253 - </div>
1254 - `);
1255 -
1256 - $('#mxchat_back_to_models').on('click', function(e) {
1257 - e.preventDefault();
1258 - // CHANGE THIS: Instead of reloading, restore the original modal content
1259 - $modalBody.html(`
1260 - <div class="mxchat-model-selector-search-container">
1261 - <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1262 - </div>
1263 - <div class="mxchat-model-selector-categories">
1264 - <button class="mxchat-model-category-btn active" data-category="all">All</button>
1265 - <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1266 - <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1267 - <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1268 - <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1269 - <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1270 - <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1271 - </div>
1272 - <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1273 - `);
1274 276
1275 - // Re-populate the grid
1276 - populateModelsGrid('', 'all');
277 + // Update tabs
278 + $('.mxchat-nav-tab').removeClass('mxchat-nav-tab-active');
279 + $this.addClass('mxchat-nav-tab-active');
1277 280
1278 - // Re-bind event handlers
1279 - rebindModalEventHandlers();
1280 - });
1281 - return;
1282 - }
1283 -
1284 - // Show loading state
1285 - $modalBody.html(`
1286 - <div style="text-align: center; padding: 60px 20px;">
1287 - <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1288 - <h3>Loading OpenRouter Models...</h3>
1289 - <p>Fetching available models from OpenRouter</p>
1290 - </div>
1291 - `);
1292 -
1293 - // Fetch models from OpenRouter
1294 - jQuery.ajax({
1295 - url: mxchatAdmin.ajax_url,
1296 - type: 'POST',
1297 - data: {
1298 - action: 'mxchat_fetch_openrouter_models',
1299 - api_key: apiKey,
1300 - nonce: mxchatAdmin.fetch_openrouter_models_nonce
1301 - },
1302 - success: function(response) {
1303 - if (response.success && response.data.models) {
1304 - displayOpenRouterModels(response.data.models);
281 + // Update content areas - with safety check
282 + $('.mxchat-tab-content').removeClass('active').hide();
283 + var $targetTab = $('#' + tabId);
284 + if ($targetTab.length) {
285 + $targetTab.addClass('active').show();
286 +
287 + // Store active tab
288 + try {
289 + localStorage.setItem('mxchat_active_tab', tabId);
290 + } catch (e) {
291 + console.warn('LocalStorage not available:', e);
292 + }
1305 293 } else {
1306 - $modalBody.html(`
1307 - <div style="text-align: center; padding: 40px;">
1308 - <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1309 - <h3>Error Loading Models</h3>
1310 - <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1311 - <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1312 - </div>
1313 - `);
1314 -
1315 - $('#mxchat_back_to_models').on('click', function(e) {
1316 - e.preventDefault();
1317 - // CHANGE THIS: Restore original content instead of reloading
1318 - restoreOriginalModalContent();
1319 - });
294 + console.warn('Tab content #' + tabId + ' not found');
1320 295 }
1321 - },
1322 - error: function() {
1323 - $modalBody.html(`
1324 - <div style="text-align: center; padding: 40px;">
1325 - <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1326 - <h3>Connection Error</h3>
1327 - <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1328 - <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1329 - </div>
1330 - `);
1331 -
1332 - $('#mxchat_back_to_models').on('click', function(e) {
1333 - e.preventDefault();
1334 - // CHANGE THIS: Restore original content instead of reloading
1335 - restoreOriginalModalContent();
1336 - });
1337 - }
1338 - });
1339 -}
1340 -function restoreOriginalModalContent() {
1341 - const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1342 -
1343 - $modalBody.html(`
1344 - <div class="mxchat-model-selector-search-container">
1345 - <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1346 - </div>
1347 - <div class="mxchat-model-selector-categories">
1348 - <button class="mxchat-model-category-btn active" data-category="all">All</button>
1349 - <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1350 - <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1351 - <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1352 - <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1353 - <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1354 - <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1355 - </div>
1356 - <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1357 - `);
1358 -
1359 - // Re-populate the grid
1360 - window.populateModelsGrid('', 'all');
1361 -
1362 - // Re-bind event handlers
1363 - rebindModalEventHandlers();
1364 -}
1365 -function rebindModalEventHandlers() {
1366 - const $modal = $('#mxchat_model_selector_modal');
1367 -
1368 - // Re-bind category button clicks
1369 - $('.mxchat-model-category-btn').off('click').on('click', function() {
1370 - $('.mxchat-model-category-btn').removeClass('active');
1371 - $(this).addClass('active');
1372 - const category = $(this).data('category');
1373 - const searchTerm = $('#mxchat_model_search_input').val();
1374 - window.populateModelsGrid(searchTerm, category);
1375 - });
1376 -
1377 - // Re-bind search input
1378 - $('#mxchat_model_search_input').off('input').on('input', function() {
1379 - const searchTerm = $(this).val();
1380 - const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1381 - populateModelsGrid(searchTerm, activeCategory);
1382 - });
1383 -}
1384 -
1385 -function restoreDefaultModalFooter() {
1386 - const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1387 -
1388 - // Restore default footer buttons
1389 - $modalFooter.html(`
1390 - <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1391 - `);
1392 -
1393 - // Re-bind cancel button
1394 - $('#mxchat_cancel_model_selection').on('click', function() {
1395 - $('#mxchat_model_selector_modal').hide();
1396 - });
1397 -}
1398 -
1399 -function displayOpenRouterModels(models) {
1400 - const $modal = $('#mxchat_model_selector_modal');
1401 - const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1402 - const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1403 - const currentSelected = $('#openrouter_selected_model').val();
1404 -
1405 - // Variable to store the currently selected model (in the UI, not yet saved)
1406 - let pendingSelection = {
1407 - modelId: currentSelected || null,
1408 - modelName: $('#openrouter_selected_model_name').val() || null
1409 - };
1410 -
1411 - // Build new modal content with search and models
1412 - const newContent = `
1413 - <div class="mxchat-model-selector-search-container">
1414 - <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1415 - <p style="margin: 10px 0; color: #666; font-size: 13px;">
1416 - <strong>${models.length} models available</strong> ·
1417 - <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1418 - </p>
1419 - </div>
1420 - <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1421 - `;
1422 -
1423 - // Update footer with Save button for OpenRouter
1424 - const footerContent = `
1425 - <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1426 - <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1427 - <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1428 - </button>
1429 - `;
1430 -
1431 - $modalBody.html(newContent);
1432 - $modalFooter.html(footerContent);
1433 -
1434 - // Function to render models
1435 - function renderOpenRouterModels(filterText = '') {
1436 - const $grid = $('#mxchat_openrouter_models_grid');
1437 - $grid.empty();
1438 -
1439 - let filteredModels = models;
1440 - if (filterText) {
1441 - const lowerFilter = filterText.toLowerCase();
1442 - filteredModels = models.filter(m =>
1443 - m.id.toLowerCase().includes(lowerFilter) ||
1444 - m.name.toLowerCase().includes(lowerFilter) ||
1445 - (m.description && m.description.toLowerCase().includes(lowerFilter))
1446 - );
1447 - }
1448 -
1449 - filteredModels.forEach(model => {
1450 - const isSelected = pendingSelection.modelId === model.id;
1451 - const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1452 - const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1453 -
1454 - const $card = jQuery(`
1455 - <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1456 - <div class="mxchat-model-selector-icon">
1457 - ${getOpenRouterIcon(model.id)}
1458 - </div>
1459 - <div class="mxchat-model-selector-info">
1460 - <h4 class="mxchat-model-selector-title">${model.name}</h4>
1461 - <div style="font-size: 12px; color: #666; margin-top: 5px;">
1462 - ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1463 - ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1464 - </div>
1465 - </div>
1466 - ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1467 - </div>
1468 - `);
1469 -
1470 - $grid.append($card);
1471 296 });
1472 297 }
1473 298
1474 - // Helper to get icon
1475 - function getOpenRouterIcon(modelId) {
1476 - if (modelId.includes('gpt') || modelId.includes('openai')) {
1477 - 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>';
1478 - } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1479 - 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>';
1480 - } else if (modelId.includes('gemini') || modelId.includes('google')) {
1481 - 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>';
1482 - }
1483 - return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1484 - }
299 + // Initialize tabs and handle events
300 + initTabs();
301 + $(document).on('widget-added widget-updated postbox-toggled', initTabs);
1485 302
1486 - // Initial render
1487 - renderOpenRouterModels();
1488 -
1489 - // Search handler
1490 - $('#mxchat_openrouter_search').on('input', function() {
1491 - renderOpenRouterModels($(this).val());
1492 - });
1493 -
1494 - $('#mxchat_back_to_provider_select').on('click', function(e) {
1495 - e.preventDefault();
1496 - // Restore footer to default state
1497 - restoreDefaultModalFooter();
1498 - // Instead of location.reload(), restore original content
1499 - restoreOriginalModalContent();
1500 - });
1501 -
1502 - // Back to providers footer button
1503 - $('#mxchat_back_to_models_footer').on('click', function(e) {
1504 - e.preventDefault();
1505 - // Restore footer to default state
1506 - restoreDefaultModalFooter();
1507 - // Restore original content
1508 - restoreOriginalModalContent();
1509 - });
1510 -
1511 - // Model selection - just highlight, don't save yet
1512 - $(document).on('click', '.mxchat-openrouter-card', function(e) {
1513 - e.stopPropagation(); // Prevent triggering the regular model card handler
1514 -
1515 - const modelId = $(this).data('model-id');
1516 - const modelName = $(this).data('model-name');
1517 -
1518 - // Remove selection from all cards
1519 - $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1520 -
1521 - // Add selection to clicked card
1522 - $(this).addClass('mxchat-model-selected');
1523 - if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1524 - $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
303 + // Activate initial tab
304 + try {
305 + var savedTab = localStorage.getItem('mxchat_active_tab');
306 + if (savedTab && $('#' + savedTab).length > 0) {
307 + $('.mxchat-nav-tab[href="#' + savedTab + '"]').trigger('click.mxchat');
308 + } else {
309 + $('.mxchat-nav-tab').first().trigger('click.mxchat');
1525 310 }
1526 -
1527 - // Update pending selection
1528 - pendingSelection.modelId = modelId;
1529 - pendingSelection.modelName = modelName;
1530 -
1531 - // Enable the save button
1532 - $('#mxchat_save_openrouter_model').prop('disabled', false);
1533 - });
1534 -
1535 - // Save button handler
1536 - $('#mxchat_save_openrouter_model').on('click', function() {
1537 - const $saveButton = $(this);
1538 -
1539 - if (!pendingSelection.modelId) {
1540 - return;
1541 - }
1542 -
1543 - // Disable button and show loading state
1544 - $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1545 -
1546 - // First, save that we're using OpenRouter
1547 - jQuery.ajax({
1548 - url: mxchatAdmin.ajax_url,
1549 - type: 'POST',
1550 - data: {
1551 - action: 'mxchat_save_setting',
1552 - name: 'model',
1553 - value: 'openrouter',
1554 - _ajax_nonce: mxchatAdmin.setting_nonce
1555 - },
1556 - success: function() {
1557 - // After model is set, save the model ID
1558 - jQuery.ajax({
1559 - url: mxchatAdmin.ajax_url,
1560 - type: 'POST',
1561 - data: {
1562 - action: 'mxchat_save_setting',
1563 - name: 'openrouter_selected_model',
1564 - value: pendingSelection.modelId,
1565 - _ajax_nonce: mxchatAdmin.setting_nonce
1566 - },
1567 - success: function() {
1568 - // Update DOM immediately
1569 - $('#openrouter_selected_model').val(pendingSelection.modelId);
1570 -
1571 - // After model ID is saved, save the display name
1572 - jQuery.ajax({
1573 - url: mxchatAdmin.ajax_url,
1574 - type: 'POST',
1575 - data: {
1576 - action: 'mxchat_save_setting',
1577 - name: 'openrouter_selected_model_name',
1578 - value: pendingSelection.modelName,
1579 - _ajax_nonce: mxchatAdmin.setting_nonce
1580 - },
1581 - success: function() {
1582 - // Update DOM immediately
1583 - $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1584 -
1585 - // Update button text
1586 - $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1587 -
1588 - // Update the "Currently using" message
1589 - const $currentSelection = $('#openrouter-current-selection');
1590 - if ($currentSelection.length) {
1591 - $currentSelection.html(
1592 - '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1593 - 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1594 - ).show();
1595 - }
1596 -
1597 - // Show success state briefly
1598 - $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1599 -
1600 - // Close modal after short delay
1601 - setTimeout(function() {
1602 - // Restore footer to default state
1603 - restoreDefaultModalFooter();
1604 - $modal.hide();
1605 - }, 800);
1606 - },
1607 - error: function() {
1608 - $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1609 - alert('Failed to save model name. Please try again.');
1610 - }
1611 - });
1612 - },
1613 - error: function() {
1614 - $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1615 - alert('Failed to save model ID. Please try again.');
1616 - }
1617 - });
1618 - },
1619 - error: function() {
1620 - $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1621 - alert('Failed to save OpenRouter selection. Please try again.');
1622 - }
1623 - });
1624 - });
1625 -
1626 -}
1627 -
1628 -// 3.2.3: Confirmation dialog shown when the user attempts to switch embedding
1629 -// models after they've already embedded content with a different model. Mixing
1630 -// embeddings from two models silently breaks similarity matching.
1631 -function showEmbeddingSwitchWarning(data, newValue, onChoice) {
1632 - $('#mxchat_embedding_switch_warning').remove();
1633 -
1634 - const dimsBlock = data.dims_differ ? `
1635 - <div class="mxchat-embed-warn-dims">
1636 - <strong>Dimension mismatch:</strong>
1637 - Existing vectors are ${data.active_dims}-dimensional, but ${data.new_label}
1638 - produces ${data.new_dims}-dimensional vectors.
1639 - If you use Pinecone, your index will reject queries entirely until you re-embed.
1640 - </div>` : '';
1641 -
1642 - const $modal = $(`
1643 - <div id="mxchat_embedding_switch_warning" class="mxchat-embed-warn-overlay">
1644 - <div class="mxchat-embed-warn-dialog">
1645 - <div class="mxchat-embed-warn-header">
1646 - <span class="mxchat-embed-warn-icon">⚠️</span>
1647 - <h3>Switching embedding models will break similarity matching</h3>
1648 - </div>
1649 - <div class="mxchat-embed-warn-body">
1650 - <p>
1651 - Your knowledge base and actions are currently embedded with
1652 - <code>${data.active_label}</code>. Switching to
1653 - <code>${data.new_label}</code> means new queries are embedded with
1654 - a different model than the stored vectors — the chatbot will return
1655 - inaccurate results or fail to match anything.
1656 - </p>
1657 - ${dimsBlock}
1658 - <p><strong>To switch safely:</strong></p>
1659 - <ol>
1660 - <li>Go to <em>Knowledge Base</em> and delete all existing entries.</li>
1661 - <li>Go to <em>Actions</em> and delete all existing actions.</li>
1662 - <li>Come back here, switch the model, then re-import your content and re-add your actions.</li>
1663 - </ol>
1664 - </div>
1665 - <div class="mxchat-embed-warn-footer">
1666 - <button type="button" class="button button-secondary" id="mxchat_embed_warn_cancel">Cancel — keep ${data.active_label}</button>
1667 - <button type="button" class="button button-primary mxchat-embed-warn-danger" id="mxchat_embed_warn_continue">Switch anyway (I'll handle it)</button>
1668 - </div>
1669 - </div>
1670 - </div>
1671 - `);
1672 -
1673 - $('body').append($modal);
1674 -
1675 - let resolved = false;
1676 - function resolve(confirmed) {
1677 - if (resolved) return;
1678 - resolved = true;
1679 - $modal.remove();
1680 - if (typeof onChoice === 'function') onChoice(confirmed);
311 + } catch (e) {
312 + $('.mxchat-nav-tab').first().trigger('click.mxchat');
1681 313 }
1682 -
1683 - // Click on the overlay background dismisses (cancel)
1684 - $modal.on('click', function(e) {
1685 - if (e.target === $modal[0]) resolve(false);
1686 - });
1687 - $modal.find('#mxchat_embed_warn_cancel').on('click', function() { resolve(false); });
1688 - $modal.find('#mxchat_embed_warn_continue').on('click', function() { resolve(true); });
1689 -}
1690 -
1691 -// Embedding model selector - completely separate from chat model selector
1692 -function setupMxChatEmbeddingModelSelector() {
1693 - const $embeddingModelSelect = $('#embedding_model');
1694 314
1695 - // Skip if the element doesn't exist on the page
1696 - if ($embeddingModelSelect.length === 0) {
1697 - return;
1698 - }
1699 -
1700 - const $embeddingModelSelectorButton = $('<button>', {
1701 - type: 'button',
1702 - id: 'mxchat_embedding_model_selector_btn',
1703 - class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1704 - text: 'Select Embedding Model'
315 + // Attach edit modal event handler
316 + $(document).on('click', '.mxchat-edit-button', function() {
317 + const intentId = $(this).data('intent-id');
318 + const phrases = $(this).data('phrases');
319 + mxchatOpenEditModal(intentId, phrases);
1705 320 });
1706 321
1707 - // Replace the select dropdown with a button
1708 - $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1709 -
1710 - // Update button text to show currently selected model
1711 - function updateButtonText() {
1712 - const selectedModel = $embeddingModelSelect.val();
1713 - const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1714 - $embeddingModelSelectorButton.text(selectedModelText);
1715 - }
1716 -
1717 - // Initialize button text
1718 - updateButtonText();
1719 -
1720 - // Create a unique ID for the modal to avoid conflicts
1721 - const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1722 -
1723 - // Create and append modal HTML with unique IDs
1724 - const embeddingModelSelectorModal = `
1725 - <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1726 - <div class="mxchat-embedding-model-selector-modal-content">
1727 - <div class="mxchat-embedding-model-selector-modal-header">
1728 - <h3>Select Embedding Model</h3>
1729 - <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1730 - </div>
1731 - <div class="mxchat-embedding-model-selector-modal-body">
1732 - <div class="mxchat-embedding-model-selector-search-container">
1733 - <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1734 - </div>
1735 - <div class="mxchat-embedding-model-selector-categories">
1736 - <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1737 - <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1738 - <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1739 - <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1740 - </div>
1741 - <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1742 - </div>
1743 - <div class="mxchat-embedding-model-selector-modal-footer">
1744 - <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1745 - </div>
1746 - </div>
1747 - </div>
1748 - `;
1749 -
1750 - // Use jQuery's append to ensure it doesn't clash with existing modals
1751 - $('body').append(embeddingModelSelectorModal);
1752 -
1753 - // Populate models grid
1754 - function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1755 - const $grid = $('#mxchat_embedding_models_grid');
1756 - $grid.empty();
1757 -
1758 - // Define embedding models with descriptions and context lengths
1759 - const models = {
1760 - openai: [
1761 - {
1762 - value: 'text-embedding-3-small',
1763 - label: 'TE3 Small',
1764 - description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1765 - },
1766 - {
1767 - value: 'text-embedding-ada-002',
1768 - label: 'Ada 2',
1769 - description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1770 - },
1771 - {
1772 - value: 'text-embedding-3-large',
1773 - label: 'TE3 Large',
1774 - description: 'High-performance embeddings (3072 dimensions, 8K context)'
1775 - }
1776 - ],
1777 - voyage: [
1778 - {
1779 - value: 'voyage-3-large',
1780 - label: 'Voyage-3 Large',
1781 - description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1782 - }
1783 - ],
1784 - gemini: [
1785 - {
1786 - value: 'gemini-embedding-001',
1787 - label: 'Gemini Embedding',
1788 - description: 'Stable SOTA embeddings (1536 dimensions, 8K context)'
1789 - }
1790 - ]
1791 - };
1792 -
1793 - let allModels = [];
1794 - Object.keys(models).forEach(key => {
1795 - if (category === 'all' || category === key) {
1796 - allModels = allModels.concat(models[key]);
1797 - }
1798 - });
1799 -
1800 - // Filter by search term if present
1801 - if (filter) {
1802 - const lowerFilter = filter.toLowerCase();
1803 - allModels = allModels.filter(model =>
1804 - model.label.toLowerCase().includes(lowerFilter) ||
1805 - model.description.toLowerCase().includes(lowerFilter)
1806 - );
1807 - }
1808 -
1809 - // Create model cards
1810 - allModels.forEach(model => {
1811 - const isSelected = $embeddingModelSelect.val() === model.value;
1812 - let providerClass = 'mxchat-embedding-model-provider-openai';
1813 -
1814 - if (model.value.startsWith('voyage-')) {
1815 - providerClass = 'mxchat-embedding-model-provider-voyage';
1816 - } else if (model.value.startsWith('gemini-embedding-')) {
1817 - providerClass = 'mxchat-embedding-model-provider-gemini';
1818 - }
1819 -
1820 - let iconHTML = '';
1821 - if (model.value.startsWith('voyage-')) {
1822 - iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1823 - } else if (model.value.startsWith('gemini-embedding-')) {
1824 - iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
322 + // Toggle visibility handlers
323 + function toggleVisibility(selector) {
324 + $(selector).on('click', function() {
325 + var inputField = $(this).prev('input');
326 + if (inputField.attr('type') === 'password') {
327 + inputField.attr('type', 'text');
328 + $(this).text('Hide');
1825 329 } else {
1826 - 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>';
330 + inputField.attr('type', 'password');
331 + $(this).text('Show');
1827 332 }
1828 -
1829 - const $modelCard = $(`
1830 - <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1831 - <div class="mxchat-embedding-model-selector-icon">
1832 - ${iconHTML}
1833 - </div>
1834 - <div class="mxchat-embedding-model-selector-info">
1835 - <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1836 - <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1837 - </div>
1838 - ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1839 - </div>
1840 - `);
1841 -
1842 - $grid.append($modelCard);
1843 333 });
1844 334 }
1845 335
1846 - // Event handlers - use namespaced events to avoid conflicts
1847 - $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1848 - e.stopPropagation(); // Prevent event bubbling
1849 - $('#' + embeddingModalId).show();
1850 - populateEmbeddingModelsGrid('', 'all');
1851 - });
336 + // Initialize all toggle visibility buttons
337 + [
338 + '#toggleApiKeyVisibility',
339 + '#toggleWooCommerceSecretVisibility',
340 + '#toggleLoopsApiKeyVisibility',
341 + '#toggleXaiApiKeyVisibility',
342 + '#toggleClaudeApiKeyVisibility',
343 + '#toggleBraveApiKeyVisibility',
344 + '#toggleWebhookUrlVisibility',
345 + '#toggleSecretKeyVisibility',
346 + '#toggleBotTokenVisibility',
347 + '#toggleDeepSeekApiKeyVisibility'
348 + ].forEach(toggleVisibility);
1852 349
1853 - $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1854 - e.stopPropagation(); // Prevent event bubbling
1855 - $('#' + embeddingModalId).hide();
1856 - });
1857 -
1858 - $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1859 - e.stopPropagation(); // Prevent event bubbling
1860 - $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1861 - $(this).addClass('active');
1862 - const category = $(this).data('category');
1863 - const searchTerm = $('#mxchat_embedding_model_search_input').val();
1864 - populateEmbeddingModelsGrid(searchTerm, category);
1865 - });
1866 -
1867 - $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1868 - const searchTerm = $(this).val();
1869 - const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1870 - populateEmbeddingModelsGrid(searchTerm, activeCategory);
1871 - });
1872 -
1873 - // 3.2.3: Commit a model change — same as the original click handler, just
1874 - // factored out so it can be invoked from both the safe path and the
1875 - // post-confirmation path.
1876 - function commitEmbeddingModelChange(modelValue) {
1877 - $embeddingModelSelect.val(modelValue);
1878 - const changeEvent = new Event('change', { bubbles: true });
1879 - $embeddingModelSelect[0].dispatchEvent(changeEvent);
1880 - updateButtonText();
1881 - $('#' + embeddingModalId).hide();
1882 - }
1883 -
1884 - // Use a direct selector to avoid conflicts with other card elements
1885 - $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1886 - e.stopPropagation(); // Prevent event bubbling
1887 - const modelValue = $(this).data('value');
1888 - const previousValue = $embeddingModelSelect.val();
1889 -
1890 - // No-op if the user clicked the already-selected card
1891 - if (modelValue === previousValue) {
1892 - $('#' + embeddingModalId).hide();
1893 - return;
1894 - }
1895 -
1896 - // 3.2.3: Preflight — if the user has content embedded with a different
1897 - // model, surface a confirmation dialog before committing the switch.
1898 - $.post(ajaxurl, {
1899 - action: 'mxchat_check_embedding_switch',
1900 - security: (typeof mxchatAdmin !== 'undefined' ? mxchatAdmin.nonce : ''),
1901 - new_model: modelValue
1902 - }).done(function(resp) {
1903 - if (resp && resp.success && resp.data && resp.data.is_mismatch) {
1904 - showEmbeddingSwitchWarning(resp.data, modelValue, function(confirmed) {
1905 - if (confirmed) {
1906 - commitEmbeddingModelChange(modelValue);
1907 - }
1908 - });
1909 - } else {
1910 - commitEmbeddingModelChange(modelValue);
1911 - }
1912 - }).fail(function() {
1913 - // If preflight fails, fall back to the original behavior so a network
1914 - // hiccup doesn't block legitimate model switches.
1915 - commitEmbeddingModelChange(modelValue);
1916 - });
1917 - return;
1918 - });
1919 -
1920 - // Close modal when clicking outside - use namespaced events
1921 - $(window).on('click.embeddingModelSelector', function(event) {
1922 - if ($(event.target).is('#' + embeddingModalId)) {
1923 - $('#' + embeddingModalId).hide();
1924 - }
1925 - });
1926 -}
1927 -
1928 -// Call this function after the DOM is fully loaded
1929 -$(document).ready(function() {
1930 - setupMxChatModelSelector();
1931 - setupMxChatEmbeddingModelSelector();
1932 -});
1933 -
1934 350 // Add Intent Form Submission
1935 351 $('#mxchat-add-intent-form').on('submit', function(event) {
1936 352 $('#mxchat-intent-loading').show();
1937 353 $('#mxchat-intent-loading-text').show();
@@ -1940,22 +356,8 @@
1940 356
1941 357 // Inline Edit Functionality
1942 358 $('.edit-button').on('click', function() {
1943 359 var row = $(this).closest('tr');
1944 -
1945 - // Clear URL field if it's a manual content URL (mxchat:// protocol)
1946 - var urlEdit = row.find('.url-edit');
1947 - if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
1948 - urlEdit.val('');
1949 - }
1950 -
1951 - // Expand the accordion to show the edit textarea (fixes short content editing)
1952 - var contentFull = row.find('.mxchat-content-full');
1953 - if (contentFull.length && contentFull.is(':hidden')) {
1954 - contentFull.show();
1955 - row.find('.mxchat-content-preview').hide();
1956 - }
1957 -
1958 360 row.find('.content-view, .url-view').hide();
1959 361 row.find('.content-edit, .url-edit').show();
1960 362 row.find('.edit-button').hide();
1961 363 row.find('.save-button').show();
@@ -1961,64 +363,97 @@
1961 363 row.find('.save-button').show();
1962 364 });
1963 365
1964 366 // Save button handler
1965 -// Save button handler
1966 -$('.save-button').on('click', function() {
1967 - var button = $(this);
1968 - var row = button.closest('tr');
1969 - var id = button.data('id');
1970 - var nonce = button.data('nonce'); // Get nonce from button data attribute
1971 - var newContent = row.find('.content-edit').val();
1972 - var newUrl = row.find('.url-edit').val();
1973 -
1974 - //console.log('Nonce from button:', nonce); // Debug
1975 -
1976 - button.prop('disabled', true);
1977 - button.text('Saving...');
1978 -
1979 - $.ajax({
1980 - url: mxchatAdmin.ajax_url,
1981 - type: 'POST',
1982 - data: {
1983 - action: 'mxchat_save_inline_prompt',
1984 - id: id,
1985 - article_content: newContent,
1986 - article_url: newUrl,
1987 - _ajax_nonce: nonce // Use nonce from button
1988 - },
1989 - success: function(response) {
1990 - button.prop('disabled', false);
1991 - button.text('Save');
1992 -
1993 - if (response.success) {
1994 - row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1995 - if (newUrl) {
1996 - row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
367 + $('.save-button').on('click', function() {
368 + var button = $(this);
369 + var row = button.closest('tr');
370 + var id = button.data('id');
371 + var newContent = row.find('.content-edit').val();
372 + var newUrl = row.find('.url-edit').val();
373 +
374 + button.prop('disabled', true);
375 + button.text('Saving...');
376 +
377 + $.ajax({
378 + url: mxchatAdmin.ajax_url,
379 + type: 'POST',
380 + data: {
381 + action: 'mxchat_save_inline_prompt',
382 + id: id,
383 + article_content: newContent,
384 + article_url: newUrl,
385 + _ajax_nonce: mxchatAdmin.inline_edit_nonce
386 + },
387 + success: function(response) {
388 + button.prop('disabled', false);
389 + button.text('Save');
390 +
391 + if (response.success) {
392 + row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
393 + if (newUrl) {
394 + row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
395 + } else {
396 + row.find('.url-view').html('N/A');
397 + }
398 +
399 + row.find('.content-edit, .url-edit').hide();
400 + row.find('.content-view, .url-view').show();
401 + row.find('.save-button').hide();
402 + row.find('.edit-button').show();
1997 403 } else {
1998 - row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
404 + alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1999 405 }
2000 -
2001 - row.find('.content-edit, .url-edit').hide();
2002 - row.find('.content-view, .url-view').show();
2003 - row.find('.save-button').hide();
2004 - row.find('.edit-button').show();
2005 -
2006 - // Restore accordion state - show preview, hide full content
2007 - row.find('.mxchat-content-preview').show();
2008 - row.find('.mxchat-content-full').hide();
406 + },
407 + error: function() {
408 + button.prop('disabled', false);
409 + button.text('Save');
410 + alert('An error occurred while saving.');
411 + }
412 + });
413 + });
414 +
415 + // Activation handling
416 + const form = $('#mxchat-activation-form');
417 + const spinner = $('#mxchat-activation-spinner');
418 + const submitButton = $('#activate_license_button');
419 + const licenseStatus = $('#mxchat-license-status');
420 +
421 + if (form.length && licenseStatus.length && submitButton.length) {
422 + function handleActivationResponse(response) {
423 + spinner.hide();
424 + if (response.success) {
425 + licenseStatus.text('Active');
426 + licenseStatus.removeClass('inactive').addClass('active');
427 + form.hide();
2009 428 } else {
2010 - alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
429 + licenseStatus.text('Inactive');
430 + alert(response.data || 'Activation failed. Please check your input.');
431 + submitButton.prop('disabled', false);
2011 432 }
2012 - },
2013 - error: function() {
2014 - button.prop('disabled', false);
2015 - button.text('Save');
2016 - alert('An error occurred while saving.');
2017 433 }
2018 - });
2019 -});
2020 434
435 + form.on('submit', function(event) {
436 + event.preventDefault();
437 + spinner.show();
438 + submitButton.prop('disabled', true);
439 +
440 + var formData = {
441 + action: 'mxchat_activate_license',
442 + mxchat_pro_email: $('#mxchat_pro_email').val(),
443 + mxchat_activation_key: $('#mxchat_activation_key').val(),
444 + security: mxchatAdmin.license_nonce
445 + };
446 +
447 + $.post(mxchatAdmin.ajax_url, formData, function(response) {
448 + handleActivationResponse(response);
449 + }).fail(function() {
450 + alert('Server error. Please try again.');
451 + spinner.hide();
452 + submitButton.prop('disabled', false);
453 + });
454 + });
455 + }
2021 456
2022 457 // Questions handling
2023 458 $('.mxchat-add-question').on('click', function () {
2024 459 const container = $('#mxchat-additional-questions-container');
@@ -2109,1526 +544,9 @@
2109 544 }
2110 545 });
2111 546 }
2112 547
2113 - // Function to adjust the textarea height to content
2114 - function adjustTextareaHeight() {
2115 - this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2116 - this.style.height = this.scrollHeight + 'px'; // Expand to content height
2117 - }
2118 -
2119 - // Function to reset the textarea height to initial
2120 - function resetTextareaHeight() {
2121 - this.style.height = ''; // Remove inline height, reverting to CSS default
2122 - }
2123 -
2124 - // Target the specific textarea by ID
2125 - var $textarea = $('#system_prompt_instructions');
2126 -
2127 - // Bind events
2128 - $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2129 - .on('blur', resetTextareaHeight); // Reset on blur
2130 -});
2131 -
2132 -
2133 -
2134 -document.addEventListener('DOMContentLoaded', function() {
2135 - // Check if we're on the correct page before initializing
2136 - const modal = document.getElementById('mxchat-action-modal');
2137 548
2138 - // Only initialize if the modal exists on this page
2139 - if (modal) {
2140 - //console.log('MXChat Action Modal JS Loaded');
2141 -
2142 - // Initialize the action modal functionality
2143 - initStepBasedActionModal();
2144 - }
2145 -
2146 - // Function to initialize the step-based action modal
2147 - function initStepBasedActionModal() {
2148 - // We already checked for modal existence above, so no need to check again
2149 -
2150 - const actionStep1 = document.getElementById('mxchat-action-step-1');
2151 - const actionStep2 = document.getElementById('mxchat-action-step-2');
2152 - const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2153 - const searchInput = document.getElementById('action-type-search');
2154 - const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2155 - const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2156 - const actionForm = document.getElementById('mxchat-action-form');
2157 - const callbackInput = document.getElementById('callback_function');
2158 - const actionIdField = document.getElementById('edit_action_id');
2159 - const labelField = document.getElementById('intent_label');
2160 - const phrasesField = document.getElementById('action_phrases');
2161 - const formActionType = document.getElementById('form_action_type');
2162 - const nonceContainer = document.getElementById('action-nonce-container');
2163 - const thresholdSlider = document.getElementById('similarity_threshold');
2164 - const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2165 -
2166 - // Rest of your initialization code remains the same...
2167 -
2168 - // Log the structure of one action card for debugging
2169 - if (actionCards.length > 0) {
2170 - //console.log('First action card data attributes:', actionCards[0].dataset);
2171 - //console.log('First action card HTML:', actionCards[0].outerHTML);
2172 - }
2173 -
2174 - // Add click event listeners to category buttons
2175 - categoryButtons.forEach(button => {
2176 - button.addEventListener('click', function() {
2177 - //console.log('Category button clicked:', this.dataset.category);
2178 -
2179 - // Remove active class from all buttons
2180 - categoryButtons.forEach(btn => btn.classList.remove('active'));
2181 -
2182 - // Add active class to clicked button
2183 - this.classList.add('active');
2184 -
2185 - // Get selected category
2186 - const category = this.dataset.category;
2187 -
2188 - // Filter action cards
2189 - filterActionCards(category, searchInput.value);
2190 - });
2191 - });
2192 -
2193 - // Add search functionality
2194 - if (searchInput) {
2195 - searchInput.addEventListener('input', function() {
2196 - // Get active category
2197 - const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2198 - //console.log('Search input changed, active category:', activeCategory);
2199 -
2200 - // Filter action cards
2201 - filterActionCards(activeCategory, this.value);
2202 - });
2203 - }
2204 -
2205 - // Add click event listeners to action cards
2206 - actionCards.forEach(card => {
2207 - card.addEventListener('click', function() {
2208 - // Get the action data
2209 - const isPro = this.dataset.pro === 'true';
2210 - const isInstalled = this.dataset.installed === 'true';
2211 - const addonName = this.dataset.addon || '';
2212 - const actionValue = this.dataset.value;
2213 - const actionLabel = this.dataset.label;
2214 - const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2215 - const actionDescription = this.querySelector('p').textContent;
2216 -
2217 - // Check if this is a promotional card (add-on not installed)
2218 - const isPromo = this.dataset.promo === 'true';
2219 -
2220 - if (isPromo || (addonName && !isInstalled)) {
2221 - // Add-on required but not installed — show informational notice
2222 - const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '').replace('— Get Add-on', '').trim() || addonName + ' Add-on';
2223 - showAddonRequiredNotice(addonDisplayName);
2224 - return;
2225 - }
2226 -
2227 - // If we get here, the action is available - proceed as normal
2228 - callbackInput.value = actionValue;
2229 -
2230 - // Update the selected action display in step 2
2231 - document.getElementById('selected-action-title').textContent = actionLabel;
2232 - document.getElementById('selected-action-description').textContent = actionDescription;
2233 - document.getElementById('selected-action-icon').innerHTML =
2234 - `<span class="dashicons dashicons-${actionIcon}"></span>`;
2235 -
2236 - // Set a default label based on the action type (user can change it)
2237 - if (!labelField.value) {
2238 - labelField.value = actionLabel;
2239 - }
2240 -
2241 - // Move to step 2
2242 - actionStep1.classList.remove('active');
2243 - actionStep2.classList.add('active');
2244 -
2245 - // Update modal title
2246 - });
2247 - });
2248 -
2249 - // Back button functionality
2250 - if (backToStep1Btn) {
2251 - backToStep1Btn.addEventListener('click', function() {
2252 - //console.log('Back button clicked');
2253 - actionStep2.classList.remove('active');
2254 - actionStep1.classList.add('active');
2255 - });
2256 - }
2257 -
2258 - // Function to filter action cards by category and search term
2259 - function filterActionCards(category, searchTerm) {
2260 - searchTerm = searchTerm.toLowerCase().trim();
2261 - //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2262 -
2263 - let visibleCount = 0;
2264 -
2265 - // Show all cards initially with animation
2266 - actionCards.forEach((card, index) => {
2267 - // Reset animation
2268 - card.style.animation = 'none';
2269 - // Trigger reflow
2270 - void card.offsetWidth;
2271 -
2272 - // Determine if card should be visible based on category and search term
2273 - const cardCategory = card.dataset.category || '';
2274 - const matchesCategory = category === 'all' || cardCategory === category;
2275 -
2276 - const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2277 - const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2278 - const matchesSearch = searchTerm === '' ||
2279 - cardTitle.includes(searchTerm) ||
2280 - cardDesc.includes(searchTerm);
2281 -
2282 - // Show/hide card with animation
2283 - if (matchesCategory && matchesSearch) {
2284 - card.style.display = 'flex';
2285 - // Staggered animation for cards
2286 - card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2287 - visibleCount++;
2288 - } else {
2289 - card.style.display = 'none';
2290 - }
2291 - });
2292 -
2293 - //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2294 - }
2295 -
2296 - // Function to show notice for Pro features
2297 - function showProFeatureNotice() {
2298 - //console.log('Showing Pro feature notice');
2299 - // Check if we already have a notification container
2300 - let noticeContainer = document.querySelector('.mxchat-pro-notice');
2301 -
2302 - if (!noticeContainer) {
2303 - // Create the notice container
2304 - noticeContainer = document.createElement('div');
2305 - noticeContainer.className = 'mxchat-pro-notice';
2306 -
2307 - // Create content
2308 - noticeContainer.innerHTML = `
2309 - <div class="mxchat-pro-notice-content">
2310 - <h3>MxChat Pro Feature</h3>
2311 - <p>This action is available in the Pro version only.</p>
2312 - <div class="mxchat-pro-notice-buttons">
2313 - <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2314 - <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2315 - </div>
2316 - </div>
2317 - `;
2318 -
2319 - // Append to body
2320 - document.body.appendChild(noticeContainer);
2321 -
2322 - // Add close functionality
2323 - const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2324 - closeButton.addEventListener('click', function() {
2325 - noticeContainer.classList.remove('active');
2326 - setTimeout(() => {
2327 - noticeContainer.remove();
2328 - }, 300);
2329 - });
2330 -
2331 - // Click outside to close
2332 - noticeContainer.addEventListener('click', function(e) {
2333 - if (e.target === noticeContainer) {
2334 - closeButton.click();
2335 - }
2336 - });
2337 -
2338 - // Show with animation
2339 - setTimeout(() => {
2340 - noticeContainer.classList.add('active');
2341 - }, 10);
2342 - } else {
2343 - // If it already exists, just make it visible again
2344 - noticeContainer.classList.add('active');
2345 - }
2346 - }
2347 -
2348 - // Function to show notice for add-on requirements
2349 - function showAddonRequiredNotice(addonName) {
2350 - //console.log(`Showing add-on notice for: ${addonName}`);
2351 - // Check if we already have a notification container
2352 - let noticeContainer = document.querySelector('.mxchat-addon-notice');
2353 -
2354 - if (!noticeContainer) {
2355 - // Create the notice container
2356 - noticeContainer = document.createElement('div');
2357 - noticeContainer.className = 'mxchat-addon-notice';
2358 -
2359 - // Create content
2360 - noticeContainer.innerHTML = `
2361 - <div class="mxchat-addon-notice-content">
2362 - <span class="mxchat-addon-notice-icon">🧩</span>
2363 - <h3>Add-on Required</h3>
2364 - <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2365 - <div class="mxchat-addon-notice-buttons">
2366 - <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2367 - <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2368 - </div>
2369 - </div>
2370 - `;
2371 -
2372 - // Append to body
2373 - document.body.appendChild(noticeContainer);
2374 -
2375 - // Add close functionality
2376 - const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2377 - closeButton.addEventListener('click', function() {
2378 - noticeContainer.classList.remove('active');
2379 - setTimeout(() => {
2380 - noticeContainer.remove();
2381 - }, 300);
2382 - });
2383 -
2384 - // Click outside to close
2385 - noticeContainer.addEventListener('click', function(e) {
2386 - if (e.target === noticeContainer) {
2387 - closeButton.click();
2388 - }
2389 - });
2390 -
2391 - // Show with animation
2392 - setTimeout(() => {
2393 - noticeContainer.classList.add('active');
2394 - }, 10);
2395 - } else {
2396 - // If it already exists, update the content
2397 - const addonNameElement = noticeContainer.querySelector('p strong');
2398 - if (addonNameElement) {
2399 - addonNameElement.textContent = addonName;
2400 - }
2401 -
2402 - // Make it visible again
2403 - noticeContainer.classList.add('active');
2404 - }
2405 - }
2406 -
2407 - // Form submission handling
2408 - if (actionForm) {
2409 - actionForm.addEventListener('submit', function() {
2410 - //console.log('Form submitted');
2411 - document.getElementById('mxchat-action-loading').style.display = 'flex';
2412 - this.querySelector('button[type="submit"]').disabled = true;
2413 - });
2414 - }
2415 - }
2416 -
2417 - // Setup add action buttons (only if we're on the correct page)
2418 - if (modal) {
2419 - // Update the modal open function to support the step-based flow
2420 - window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2421 - //console.log('Modal opening, edit mode:', isEdit);
2422 -
2423 - // Get form fields
2424 - const actionIdField = document.getElementById('edit_action_id');
2425 - const labelField = document.getElementById('intent_label');
2426 - const phrasesField = document.getElementById('action_phrases');
2427 - const formActionType = document.getElementById('form_action_type');
2428 - const callbackInput = document.getElementById('callback_function');
2429 - const saveButton = document.getElementById('mxchat-save-action-btn');
2430 - const nonceContainer = document.getElementById('action-nonce-container');
2431 - const thresholdSlider = document.getElementById('similarity_threshold');
2432 - const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2433 - const actionStep1 = document.getElementById('mxchat-action-step-1');
2434 - const actionStep2 = document.getElementById('mxchat-action-step-2');
2435 - const searchInput = document.getElementById('action-type-search');
2436 -
2437 - // Set up modal for edit or create
2438 - if (isEdit) {
2439 - saveButton.textContent = 'Update Action';
2440 - formActionType.value = 'mxchat_edit_intent';
2441 - actionIdField.value = actionId;
2442 - labelField.value = label;
2443 - phrasesField.value = phrases;
2444 - callbackInput.value = callbackFunction;
2445 - thresholdSlider.value = threshold; // Set the current threshold value
2446 - thresholdDisplay.textContent = threshold + '%'; // Update display
2447 -
2448 - // NEW: Handle bot selection checkboxes for editing
2449 - // First uncheck all bot checkboxes
2450 - document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2451 - checkbox.checked = false;
2452 - });
2453 -
2454 - // Then check the ones that should be enabled
2455 - if (enabledBots) {
2456 - let botsArray;
2457 - if (typeof enabledBots === 'string') {
2458 - try {
2459 - botsArray = JSON.parse(enabledBots);
2460 - } catch (e) {
2461 - console.warn('Failed to parse enabledBots:', enabledBots);
2462 - botsArray = ['default']; // fallback
2463 - }
2464 - } else if (Array.isArray(enabledBots)) {
2465 - botsArray = enabledBots;
2466 - } else {
2467 - botsArray = ['default']; // fallback
2468 - }
2469 -
2470 - botsArray.forEach(botId => {
2471 - const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2472 - if (checkbox) {
2473 - checkbox.checked = true;
2474 - }
2475 - });
2476 - } else {
2477 - // Fallback to default if no bot data
2478 - const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2479 - if (defaultCheckbox) {
2480 - defaultCheckbox.checked = true;
2481 - }
2482 - }
2483 -
2484 - // Update the nonce field for editing
2485 - nonceContainer.innerHTML = ''; // Clear existing nonce
2486 - if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2487 - nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2488 - }
2489 -
2490 - // For editing, go directly to step 2 and update the selected action display
2491 - actionStep1.classList.remove('active');
2492 - actionStep2.classList.add('active');
2493 -
2494 - // Find the matching action card to get its details
2495 - const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2496 - let foundCard = null;
2497 -
2498 - actionCards.forEach(card => {
2499 - if (card.dataset.value === callbackFunction) {
2500 - foundCard = card;
2501 - }
2502 - });
2503 -
2504 - if (foundCard) {
2505 - //console.log('Found matching action card for:', callbackFunction);
2506 - const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2507 - const actionIconElement = foundCard.querySelector('.dashicons');
2508 - const actionIcon = actionIconElement
2509 - ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2510 - : 'admin-generic';
2511 - const actionDescription = foundCard.querySelector('p')?.textContent || '';
2512 -
2513 - document.getElementById('selected-action-title').textContent = actionLabel;
2514 - document.getElementById('selected-action-description').textContent = actionDescription;
2515 - document.getElementById('selected-action-icon').innerHTML =
2516 - `<span class="dashicons dashicons-${actionIcon}"></span>`;
2517 - } else {
2518 - //console.log('No matching action card found for:', callbackFunction);
2519 - // Fallback if we can't find the card
2520 - document.getElementById('selected-action-title').textContent = label;
2521 - document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2522 - document.getElementById('selected-action-icon').innerHTML =
2523 - `<span class="dashicons dashicons-admin-generic"></span>`;
2524 - }
2525 - } else {
2526 - //console.log('Setting up create mode');
2527 - saveButton.textContent = 'Save Action';
2528 - formActionType.value = 'mxchat_add_intent';
2529 - actionIdField.value = '';
2530 - labelField.value = '';
2531 - phrasesField.value = '';
2532 - callbackInput.value = '';
2533 - thresholdSlider.value = 85; // Default value for new actions
2534 - thresholdDisplay.textContent = '85%'; // Default display
2535 -
2536 - // For new actions, ensure default is checked and others are unchecked
2537 - document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2538 - checkbox.checked = (checkbox.value === 'default');
2539 - });
2540 -
2541 - // Update the nonce field for adding
2542 - nonceContainer.innerHTML = ''; // Clear existing nonce
2543 - if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2544 - nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2545 - }
2546 -
2547 - // For creating new, start at step 1
2548 - actionStep1.classList.add('active');
2549 - actionStep2.classList.remove('active');
2550 - }
2551 -
2552 - // Show modal with animation
2553 - modal.style.display = 'flex';
2554 - requestAnimationFrame(() => {
2555 - modal.classList.add('active');
2556 - });
2557 -
2558 - // Set up close handlers
2559 - const closeModal = () => {
2560 - //console.log('Closing modal');
2561 - modal.classList.remove('active');
2562 - setTimeout(() => {
2563 - modal.style.display = 'none';
2564 - }, 300); // Match the CSS transition time
2565 - };
2566 -
2567 - // Close button handler
2568 - const closeBtn = modal.querySelector('.mxchat-modal-close');
2569 - if (closeBtn) {
2570 - closeBtn.onclick = closeModal;
2571 - }
2572 -
2573 - // Cancel button handler
2574 - const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2575 - if (cancelBtns) {
2576 - cancelBtns.forEach(btn => {
2577 - btn.onclick = closeModal;
2578 - });
2579 - }
2580 -
2581 - // Click outside modal to close
2582 - modal.onclick = (e) => {
2583 - if (e.target === modal) {
2584 - closeModal();
2585 - }
2586 - };
2587 -
2588 - // Escape key to close modal
2589 - document.addEventListener('keydown', function(e) {
2590 - if (e.key === 'Escape' && modal.classList.contains('active')) {
2591 - closeModal();
2592 - }
2593 - }, { once: true });
2594 -
2595 - // Focus appropriate field based on current step
2596 - if (isEdit || actionStep2.classList.contains('active')) {
2597 - if (labelField) labelField.focus();
2598 - } else {
2599 - if (searchInput) searchInput.focus();
2600 - }
2601 -
2602 - return closeModal; // Return close function for external use
2603 - };
2604 - // Setup add action buttons
2605 - const addActionBtn = document.getElementById('mxchat-add-action-btn');
2606 - if (addActionBtn) {
2607 - //console.log('Add action button found');
2608 - addActionBtn.onclick = () => window.mxchatOpenActionModal();
2609 - }
2610 -
2611 - const createFirstAction = document.getElementById('mxchat-create-first-action');
2612 - if (createFirstAction) {
2613 - //console.log('Create first action button found');
2614 - createFirstAction.onclick = () => window.mxchatOpenActionModal();
2615 - }
2616 -
2617 - // Setup edit buttons
2618 - const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2619 - //console.log('Edit buttons found:', editButtons.length);
2620 - editButtons.forEach(button => {
2621 - button.onclick = () => {
2622 - const actionId = button.dataset.actionId;
2623 - const phrases = button.dataset.phrases;
2624 - const label = button.dataset.label;
2625 - const threshold = button.dataset.threshold || 85;
2626 - const callbackFunction = button.dataset.callbackFunction;
2627 - const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2628 -
2629 - window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2630 - };
2631 - });
2632 - }
2633 549 });
2634 550
2635 -jQuery(document).ready(function($) {
2636 - // Auto-expand custom post types container if any are checked
2637 - // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2638 - function autoExpandIfNeeded() {
2639 - const $container = $('#mxchat-custom-post-types-container');
2640 - const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2641 551
2642 - if ($container.length === 0) return;
2643 552
2644 - const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2645 -
2646 - if (hasCheckedItems) {
2647 - $container.show();
2648 - const $icon = $toggleBtn.find('span:last-child');
2649 - if ($icon.length) {
2650 - $icon.text('▲');
2651 - }
2652 - }
2653 - }
2654 -
2655 - // Run on page load
2656 - autoExpandIfNeeded();
2657 -});
2658 -
2659 -document.addEventListener('DOMContentLoaded', function() {
2660 - var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2661 - var modal = document.getElementById('mxchatSampleModal');
2662 - var modalClose = document.getElementById('mxchatModalClose');
2663 - var closeBtn = document.getElementById('mxchatCloseBtn');
2664 - var copyBtn = document.getElementById('mxchatCopyBtn');
2665 - var instructionsContent = document.querySelector('.mxchat-instructions-content');
2666 - var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2667 -
2668 - if (!viewSampleBtn || !modal) {
2669 - return;
2670 - }
2671 -
2672 - // Open modal
2673 - viewSampleBtn.addEventListener('click', function(e) {
2674 - e.preventDefault();
2675 - e.stopPropagation();
2676 - modal.classList.add('mxchat-instructions-show');
2677 - });
2678 -
2679 - // Close modal function
2680 - function closeModal(e) {
2681 - if (e) {
2682 - e.preventDefault();
2683 - e.stopPropagation();
2684 - }
2685 - modal.classList.remove('mxchat-instructions-show');
2686 - }
2687 -
2688 - // Close modal events
2689 - if (modalClose) {
2690 - modalClose.addEventListener('click', function(e) {
2691 - closeModal(e);
2692 - });
2693 - }
2694 -
2695 - if (closeBtn) {
2696 - closeBtn.addEventListener('click', function(e) {
2697 - closeModal(e);
2698 - });
2699 - }
2700 -
2701 - // Close on backdrop click ONLY (not on hover)
2702 - modal.addEventListener('click', function(e) {
2703 - // Only close if clicking directly on the overlay, not on child elements
2704 - if (e.target === modal) {
2705 - closeModal(e);
2706 - }
2707 - });
2708 -
2709 - // Prevent modal content clicks from closing the modal
2710 - if (modalContent) {
2711 - modalContent.addEventListener('click', function(e) {
2712 - e.stopPropagation();
2713 - });
2714 - }
2715 -
2716 - // Close on escape key
2717 - document.addEventListener('keydown', function(e) {
2718 - if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2719 - closeModal();
2720 - }
2721 - });
2722 -
2723 - // Copy functionality
2724 - if (copyBtn && instructionsContent) {
2725 - copyBtn.addEventListener('click', function(e) {
2726 - e.preventDefault();
2727 - e.stopPropagation();
2728 -
2729 - var text = instructionsContent.textContent;
2730 -
2731 - if (navigator.clipboard) {
2732 - navigator.clipboard.writeText(text).then(function() {
2733 - showCopySuccess();
2734 - }).catch(function() {
2735 - fallbackCopy(text);
2736 - });
2737 - } else {
2738 - fallbackCopy(text);
2739 - }
2740 - });
2741 - }
2742 -
2743 - function fallbackCopy(text) {
2744 - var textArea = document.createElement('textarea');
2745 - textArea.value = text;
2746 - textArea.style.position = 'fixed';
2747 - textArea.style.left = '-999999px';
2748 - textArea.style.top = '-999999px';
2749 - document.body.appendChild(textArea);
2750 - textArea.select();
2751 - try {
2752 - document.execCommand('copy');
2753 - showCopySuccess();
2754 - } catch (err) {
2755 - console.error('Copy failed');
2756 - }
2757 - document.body.removeChild(textArea);
2758 - }
2759 -
2760 - function showCopySuccess() {
2761 - var originalText = copyBtn.innerHTML;
2762 - 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!';
2763 -
2764 - setTimeout(function() {
2765 - copyBtn.innerHTML = originalText;
2766 - }, 2000);
2767 - }
2768 -});
2769 -
2770 -jQuery(document).ready(function($) {
2771 - var emailDependentFields = [
2772 - '#email_blocker_header_content',
2773 - '#email_blocker_button_text',
2774 - '#enable_name_field',
2775 - '#name_field_placeholder'
2776 - ];
2777 -
2778 - // Add visual indicators
2779 - emailDependentFields.forEach(function(fieldId) {
2780 - var $row = $(fieldId).closest('tr');
2781 - $row.addClass('email-dependent-field');
2782 -
2783 - // Add an icon to show it's dependent
2784 - var $label = $row.find('th label, th');
2785 - $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2786 - });
2787 -
2788 - // Hide initially with opacity for smoother transition
2789 - emailDependentFields.forEach(function(fieldId) {
2790 - $(fieldId).closest('tr').hide().css('opacity', '0');
2791 - });
2792 -
2793 - function toggleEmailFields() {
2794 - var emailEnabled = $('#enable_email_block').is(':checked');
2795 -
2796 - emailDependentFields.forEach(function(fieldId) {
2797 - var $row = $(fieldId).closest('tr');
2798 - if (emailEnabled) {
2799 - $row.slideDown(400).animate({opacity: 1}, 200);
2800 - } else {
2801 - $row.animate({opacity: 0}, 200).slideUp(400);
2802 - }
2803 - });
2804 - }
2805 -
2806 - toggleEmailFields();
2807 - $('#enable_email_block').on('change', toggleEmailFields);
2808 -});
2809 -
2810 -
2811 -
2812 -//Handle role restriction changes
2813 -jQuery(document).ready(function($) {
2814 - //Handle role restriction changes for both data sources
2815 - $(document).on('change', '.mxchat-role-select', function() {
2816 - const $select = $(this);
2817 - const entryId = $select.data('entry-id');
2818 - const dataSource = $select.data('data-source');
2819 - const roleRestriction = $select.val();
2820 - const nonce = $select.data('nonce');
2821 -
2822 - // Visual feedback
2823 - $select.prop('disabled', true).addClass('updating');
2824 -
2825 - $.ajax({
2826 - url: ajaxurl,
2827 - type: 'POST',
2828 - data: {
2829 - action: 'mxchat_update_role_restriction',
2830 - nonce: nonce,
2831 - entry_id: entryId,
2832 - data_source: dataSource, // NEW: Include data source
2833 - role_restriction: roleRestriction
2834 - },
2835 - success: function(response) {
2836 - if (response.success) {
2837 - // Show success feedback
2838 - $select.removeClass('updating').addClass('updated');
2839 - setTimeout(() => {
2840 - $select.removeClass('updated');
2841 - }, 2000);
2842 - } else {
2843 - alert('Failed to update role restriction: ' + response.data);
2844 - // Revert selection
2845 - $select.val($select.data('original-value'));
2846 - }
2847 - },
2848 - error: function() {
2849 - alert('Error updating role restriction');
2850 - // Revert selection
2851 - $select.val($select.data('original-value'));
2852 - },
2853 - complete: function() {
2854 - $select.prop('disabled', false);
2855 - }
2856 - });
2857 -
2858 - // Store original value for potential revert
2859 - $select.data('original-value', roleRestriction);
2860 - });
2861 -
2862 - // Store initial values
2863 - $('.mxchat-role-select').each(function() {
2864 - $(this).data('original-value', $(this).val());
2865 - });
2866 -});
2867 -
2868 -// Bot Selector Handler
2869 -jQuery(document).ready(function($) {
2870 - var saveTimer;
2871 -
2872 - // Function to update bot_id in forms
2873 - function updateBotIdInForm(formSelector) {
2874 - var botId = $('#mxchat-bot-selector').val();
2875 - var form = $(formSelector);
2876 -
2877 - if (form.length > 0) {
2878 - // Remove existing bot_id hidden input
2879 - form.find('input[name="bot_id"]').remove();
2880 -
2881 - // Add new bot_id hidden input if not default
2882 - if (botId && botId !== 'default') {
2883 - form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2884 - }
2885 - }
2886 - }
2887 -
2888 - $('#mxchat-bot-selector').on('change', function() {
2889 - var botId = $(this).val();
2890 -
2891 - // Clear any existing timer
2892 - clearTimeout(saveTimer);
2893 -
2894 - // Update all forms with new bot_id when bot selection changes
2895 - updateBotIdInForm('#mxchat-url-form');
2896 - updateBotIdInForm('#mxchat-content-form');
2897 -
2898 - // Save the selection via AJAX
2899 - $.ajax({
2900 - url: ajaxurl,
2901 - type: 'POST',
2902 - data: {
2903 - action: 'mxchat_save_selected_bot',
2904 - bot_id: botId,
2905 - nonce: mxchatAdmin.setting_nonce
2906 - },
2907 - success: function(response) {
2908 - if (response.success) {
2909 - // Show saved indicator
2910 - $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2911 -
2912 - // Reload the page after a short delay to refresh content
2913 - saveTimer = setTimeout(function() {
2914 - var currentUrl = new URL(window.location.href);
2915 - currentUrl.searchParams.set('bot_id', botId);
2916 - currentUrl.searchParams.set('page', 'mxchat-prompts');
2917 - window.location.href = currentUrl.toString();
2918 - }, 500);
2919 - }
2920 - },
2921 - error: function() {
2922 - console.error('Failed to save bot selection');
2923 - }
2924 - });
2925 - });
2926 -
2927 - // Initialize forms with current bot_id when the page loads
2928 - setTimeout(function() {
2929 - updateBotIdInForm('#mxchat-url-form');
2930 - updateBotIdInForm('#mxchat-content-form');
2931 - }, 100);
2932 -});
2933 -
2934 -// API Key Status Indicator for Chat Models
2935 -jQuery(document).ready(function($) {
2936 - function updateChatModelAPIStatus(apiKeyStatuses) {
2937 - var selectedModel = $('#model').val();
2938 -
2939 - // Hide all status messages
2940 - $('.mxchat-api-status').hide();
2941 -
2942 - // Return early if no model is selected
2943 - if (!selectedModel) {
2944 - return;
2945 - }
2946 -
2947 - // Map models to providers
2948 - var provider = null;
2949 -
2950 - if (selectedModel.startsWith('gpt-')) {
2951 - provider = 'openai';
2952 - } else if (selectedModel.startsWith('claude-')) {
2953 - provider = 'claude';
2954 - } else if (selectedModel.startsWith('grok-')) {
2955 - provider = 'xai';
2956 - } else if (selectedModel.startsWith('deepseek-')) {
2957 - provider = 'deepseek';
2958 - } else if (selectedModel.startsWith('gemini-')) {
2959 - provider = 'gemini';
2960 - } else if (selectedModel === 'openrouter') {
2961 - provider = 'openrouter';
2962 - }
2963 -
2964 - // If we have fresh API key data, update the messages
2965 - if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2966 - var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
2967 - var hasKey = apiKeyStatuses[provider];
2968 -
2969 - if (hasKey) {
2970 - $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2971 - } else {
2972 - $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2973 - }
2974 - }
2975 -
2976 - // Show the appropriate status message
2977 - if (provider) {
2978 - $('.mxchat-api-status[data-provider="' + provider + '"]').show();
2979 - }
2980 - }
2981 -
2982 - function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
2983 - var selectedModel = $('#embedding_model').val();
2984 -
2985 - // Hide all status messages
2986 - $('.mxchat-embedding-api-status').hide();
2987 -
2988 - // Return early if no model is selected
2989 - if (!selectedModel) {
2990 - return;
2991 - }
2992 -
2993 - // Map models to providers
2994 - var provider = null;
2995 -
2996 - if (selectedModel.startsWith('text-embedding-')) {
2997 - provider = 'openai';
2998 - } else if (selectedModel.startsWith('voyage-')) {
2999 - provider = 'voyage';
3000 - } else if (selectedModel.startsWith('gemini-embedding-')) {
3001 - provider = 'gemini';
3002 - }
3003 -
3004 - // If we have fresh API key data, update the messages
3005 - if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3006 - var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
3007 - var hasKey = apiKeyStatuses[provider];
3008 -
3009 - if (hasKey) {
3010 - $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3011 - } else {
3012 - $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3013 - }
3014 - }
3015 -
3016 - // Show the appropriate status message
3017 - if (provider) {
3018 - $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
3019 - }
3020 - }
3021 -
3022 - function getProviderName(provider) {
3023 - var names = {
3024 - 'openai': 'OpenAI',
3025 - 'claude': 'Anthropic (Claude)',
3026 - 'xai': 'X.AI (Grok)',
3027 - 'deepseek': 'DeepSeek',
3028 - 'gemini': 'Google Gemini',
3029 - 'openrouter': 'OpenRouter',
3030 - 'voyage': 'Voyage AI'
3031 - };
3032 - return names[provider] || provider;
3033 - }
3034 -
3035 - function refreshAPIKeyStatus() {
3036 - $.ajax({
3037 - url: ajaxurl,
3038 - type: 'POST',
3039 - data: {
3040 - action: 'mxchat_check_api_keys',
3041 - nonce: mxchatAdmin.setting_nonce
3042 - },
3043 - success: function(response) {
3044 - if (response.success && response.data) {
3045 - updateChatModelAPIStatus(response.data);
3046 - updateEmbeddingModelAPIStatus(response.data);
3047 - }
3048 - }
3049 - });
3050 - }
3051 -
3052 - // Expose refresh function globally so auto-save can call it
3053 - window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
3054 -
3055 - // Run on page load
3056 - updateChatModelAPIStatus();
3057 - updateEmbeddingModelAPIStatus();
3058 -
3059 - // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
3060 - var urlParams = new URLSearchParams(window.location.search);
3061 - if (urlParams.get('settings-updated') === 'true') {
3062 - // Page was just reloaded after save, fetch fresh API key status
3063 - refreshAPIKeyStatus();
3064 - }
3065 -
3066 - // Run when model changes
3067 - $('#model').on('change', function() {
3068 - updateChatModelAPIStatus();
3069 - updateWebSearchToggleVisibility();
3070 - });
3071 - $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
3072 -
3073 - // Web Search toggle visibility based on model
3074 - function updateWebSearchToggleVisibility() {
3075 - var selectedModel = $('#model').val();
3076 - var $wrapper = $('#web-search-toggle-wrapper');
3077 - var $unavailableMessage = $('#web-search-unavailable-message');
3078 -
3079 - if (!$wrapper.length) return; // Element doesn't exist
3080 -
3081 - // Get the list of supported OpenAI models from data attribute
3082 - var openaiModelsAttr = $wrapper.data('openai-models');
3083 - var unsupportedModelsAttr = $wrapper.data('unsupported-models');
3084 -
3085 - var openaiModels = openaiModelsAttr ? openaiModelsAttr.split(',') : [];
3086 - var unsupportedModels = unsupportedModelsAttr ? unsupportedModelsAttr.split(',') : [];
3087 -
3088 - // Check if selected model is an OpenAI model that supports web search
3089 - var isOpenAI = openaiModels.includes(selectedModel);
3090 - var isSupported = isOpenAI && !unsupportedModels.includes(selectedModel);
3091 -
3092 - if (isSupported) {
3093 - $wrapper.show();
3094 - $unavailableMessage.hide();
3095 - } else {
3096 - $wrapper.hide();
3097 - $unavailableMessage.show();
3098 - }
3099 - }
3100 -
3101 - // Run on page load
3102 - updateWebSearchToggleVisibility();
3103 -});
3104 -
3105 -// Transcripts Metrics Dashboard
3106 -jQuery(document).ready(function($) {
3107 - // Tab switching functionality
3108 - $('.mxchat-metrics-tab').on('click', function() {
3109 - const tabName = $(this).data('tab');
3110 -
3111 - // Update tab buttons
3112 - $('.mxchat-metrics-tab').removeClass('active');
3113 - $(this).addClass('active');
3114 -
3115 - // Update panels
3116 - $('.mxchat-metrics-panel').removeClass('active');
3117 - $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3118 -
3119 - // Initialize chart if activity tab is shown
3120 - if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3121 - // Small delay to ensure the canvas is visible
3122 - setTimeout(function() {
3123 - initActivityChart();
3124 - }, 50);
3125 - }
3126 - });
3127 -
3128 - // Initialize chart function
3129 - function initActivityChart() {
3130 - const canvas = document.getElementById('mxchat-activity-chart');
3131 - if (!canvas) return;
3132 -
3133 - // Check if chart already exists and destroy it
3134 - if (canvas.chartInstance) {
3135 - canvas.chartInstance.destroy();
3136 - }
3137 -
3138 - const ctx = canvas.getContext('2d');
3139 -
3140 - // Create gradient for chats line
3141 - const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3142 - chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3143 - chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3144 -
3145 - // Create gradient for messages line
3146 - const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3147 - messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3148 - messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3149 -
3150 - // Simple chart without external library
3151 - canvas.chartInstance = new SimpleChart(canvas, {
3152 - labels: mxchatChartData.labels,
3153 - datasets: [
3154 - {
3155 - label: 'Chats',
3156 - data: mxchatChartData.chats,
3157 - borderColor: '#667eea',
3158 - backgroundColor: chatsGradient,
3159 - fill: true
3160 - },
3161 - {
3162 - label: 'Messages',
3163 - data: mxchatChartData.messages,
3164 - borderColor: '#764ba2',
3165 - backgroundColor: messagesGradient,
3166 - fill: true
3167 - }
3168 - ]
3169 - });
3170 - }
3171 -
3172 - // Simple chart implementation (no external dependencies)
3173 - class SimpleChart {
3174 - constructor(canvas, config) {
3175 - this.canvas = canvas;
3176 - this.ctx = canvas.getContext('2d');
3177 - this.config = config;
3178 - this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3179 - this.render();
3180 - }
3181 -
3182 - destroy() {
3183 - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3184 - }
3185 -
3186 - render() {
3187 - const dpr = window.devicePixelRatio || 1;
3188 - const rect = this.canvas.getBoundingClientRect();
3189 -
3190 - this.canvas.width = rect.width * dpr;
3191 - this.canvas.height = rect.height * dpr;
3192 - this.ctx.scale(dpr, dpr);
3193 -
3194 - this.canvas.style.width = rect.width + 'px';
3195 - this.canvas.style.height = rect.height + 'px';
3196 -
3197 - const width = rect.width - this.padding.left - this.padding.right;
3198 - const height = rect.height - this.padding.top - this.padding.bottom;
3199 -
3200 - // Find max value
3201 - let maxValue = 0;
3202 - this.config.datasets.forEach(dataset => {
3203 - const max = Math.max(...dataset.data);
3204 - if (max > maxValue) maxValue = max;
3205 - });
3206 -
3207 - // Add some padding to max value
3208 - maxValue = Math.ceil(maxValue * 1.1);
3209 - if (maxValue === 0) maxValue = 10;
3210 -
3211 - // Draw grid lines
3212 - this.ctx.strokeStyle = '#e5e7eb';
3213 - this.ctx.lineWidth = 1;
3214 - const gridLines = 5;
3215 -
3216 - for (let i = 0; i <= gridLines; i++) {
3217 - const y = this.padding.top + (height / gridLines) * i;
3218 - this.ctx.beginPath();
3219 - this.ctx.moveTo(this.padding.left, y);
3220 - this.ctx.lineTo(this.padding.left + width, y);
3221 - this.ctx.stroke();
3222 -
3223 - // Draw y-axis labels
3224 - const value = maxValue - (maxValue / gridLines) * i;
3225 - this.ctx.fillStyle = '#6b7280';
3226 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3227 - this.ctx.textAlign = 'right';
3228 - this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3229 - }
3230 -
3231 - // Draw datasets
3232 - this.config.datasets.forEach(dataset => {
3233 - const points = [];
3234 - const xStep = width / (this.config.labels.length - 1 || 1);
3235 -
3236 - dataset.data.forEach((value, index) => {
3237 - const x = this.padding.left + (xStep * index);
3238 - const y = this.padding.top + height - (value / maxValue * height);
3239 - points.push({ x, y, value });
3240 - });
3241 -
3242 - // Draw filled area
3243 - if (dataset.fill && dataset.backgroundColor) {
3244 - this.ctx.fillStyle = dataset.backgroundColor;
3245 - this.ctx.beginPath();
3246 - this.ctx.moveTo(points[0].x, this.padding.top + height);
3247 - points.forEach(point => {
3248 - this.ctx.lineTo(point.x, point.y);
3249 - });
3250 - this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3251 - this.ctx.closePath();
3252 - this.ctx.fill();
3253 - }
3254 -
3255 - // Draw line
3256 - this.ctx.strokeStyle = dataset.borderColor;
3257 - this.ctx.lineWidth = 3;
3258 - this.ctx.lineCap = 'round';
3259 - this.ctx.lineJoin = 'round';
3260 -
3261 - this.ctx.beginPath();
3262 - points.forEach((point, index) => {
3263 - if (index === 0) {
3264 - this.ctx.moveTo(point.x, point.y);
3265 - } else {
3266 - this.ctx.lineTo(point.x, point.y);
3267 - }
3268 - });
3269 - this.ctx.stroke();
3270 -
3271 - // Draw points
3272 - points.forEach(point => {
3273 - this.ctx.fillStyle = '#ffffff';
3274 - this.ctx.beginPath();
3275 - this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3276 - this.ctx.fill();
3277 - this.ctx.strokeStyle = dataset.borderColor;
3278 - this.ctx.lineWidth = 2;
3279 - this.ctx.stroke();
3280 - });
3281 - });
3282 -
3283 - // Draw x-axis labels
3284 - const xStep = width / (this.config.labels.length - 1 || 1);
3285 - this.ctx.fillStyle = '#6b7280';
3286 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3287 - this.ctx.textAlign = 'center';
3288 -
3289 - this.config.labels.forEach((label, index) => {
3290 - const x = this.padding.left + (xStep * index);
3291 - this.ctx.fillText(label, x, this.padding.top + height + 20);
3292 - });
3293 -
3294 - // Draw legend
3295 - let legendX = this.padding.left;
3296 - const legendY = rect.height - 10;
3297 -
3298 - this.config.datasets.forEach((dataset, index) => {
3299 - // Color box
3300 - this.ctx.fillStyle = dataset.borderColor;
3301 - this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3302 -
3303 - // Label
3304 - this.ctx.fillStyle = '#374151';
3305 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3306 - this.ctx.textAlign = 'left';
3307 - this.ctx.fillText(dataset.label, legendX + 18, legendY);
3308 -
3309 - legendX += this.ctx.measureText(dataset.label).width + 40;
3310 - });
3311 - }
3312 - }
3313 -
3314 - // Initialize chart on page load if we're on the activity tab
3315 - if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3316 - setTimeout(function() {
3317 - initActivityChart();
3318 - }, 100);
3319 - }
3320 -
3321 - // ========================================
3322 - // SLACK TEST CONNECTION
3323 - // ========================================
3324 - $('#mxchat-test-slack-connection').on('click', function() {
3325 - var $button = $(this);
3326 - var $result = $('#mxchat-slack-test-result');
3327 - var originalText = $button.html();
3328 -
3329 - // Show loading state
3330 - $button.prop('disabled', true).html(
3331 - '<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>' +
3332 - 'Testing...'
3333 - );
3334 -
3335 - $.ajax({
3336 - url: ajaxurl,
3337 - type: 'POST',
3338 - data: {
3339 - action: 'mxchat_test_slack_connection',
3340 - nonce: mxchatAdmin.nonce
3341 - },
3342 - success: function(response) {
3343 - $result.show();
3344 - if (response.success) {
3345 - $result.html(
3346 - '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3347 - '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3348 - '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3349 - '</div>'
3350 - );
3351 - } else {
3352 - var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3353 - var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3354 - var textColor = response.data.partial ? '#856404' : '#721c24';
3355 - var icon = response.data.partial ? '⚠' : '✗';
3356 - var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3357 -
3358 - $result.html(
3359 - '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3360 - '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3361 - '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3362 - (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>' : '') +
3363 - '</div>'
3364 - );
3365 - }
3366 - },
3367 - error: function() {
3368 - $result.show().html(
3369 - '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3370 - '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3371 - '</div>'
3372 - );
3373 - },
3374 - complete: function() {
3375 - $button.prop('disabled', false).html(originalText);
3376 - }
3377 - });
3378 - });
3379 -
3380 - // Helper function to escape HTML
3381 - function escapeHtml(text) {
3382 - var div = document.createElement('div');
3383 - div.appendChild(document.createTextNode(text));
3384 - return div.innerHTML;
3385 - }
3386 -
3387 - // ========================================
3388 - // DEBUG & OPTIMIZATION TOOLS
3389 - // ========================================
3390 -
3391 - // Debug Mode Toggle
3392 - $('#mxchat_debug_mode').on('change', function() {
3393 - var $toggle = $(this);
3394 - var enabled = $toggle.is(':checked') ? 'on' : 'off';
3395 - var $label = $toggle.closest('label');
3396 -
3397 - // Add loading indicator next to the toggle label
3398 - var $indicator = $label.find('.mxchat-save-indicator');
3399 - if ($indicator.length === 0) {
3400 - $indicator = $('<span class="mxchat-save-indicator" style="margin-left: 10px;"></span>');
3401 - $label.append($indicator);
3402 - }
3403 - $indicator.html('<span class="mxchat-saving-spinner"></span>').show();
3404 -
3405 - $.ajax({
3406 - url: ajaxurl,
3407 - type: 'POST',
3408 - data: {
3409 - action: 'mxchat_toggle_debug_mode',
3410 - enabled: enabled,
3411 - _ajax_nonce: mxchatAdmin.setting_nonce
3412 - },
3413 - success: function(response) {
3414 - if (response.success) {
3415 - // Show success indicator
3416 - $indicator.html('<span class="mxchat-save-success">✓</span>');
3417 - setTimeout(function() {
3418 - $indicator.fadeOut(300);
3419 - }, 2000);
3420 -
3421 - // Always refresh the log after toggling
3422 - refreshDebugLog();
3423 - } else {
3424 - $indicator.html('<span class="mxchat-save-error">✗</span>');
3425 - alert(response.data.message || 'Error toggling debug mode');
3426 - $toggle.prop('checked', !$toggle.is(':checked'));
3427 - }
3428 - },
3429 - error: function() {
3430 - $indicator.html('<span class="mxchat-save-error">✗</span>');
3431 - alert('Error toggling debug mode');
3432 - $toggle.prop('checked', !$toggle.is(':checked'));
3433 - }
3434 - });
3435 - });
3436 -
3437 - // Refresh Debug Log
3438 - function refreshDebugLog() {
3439 - var $container = $('#mxchat-debug-log');
3440 - var $countBadge = $('#mxchat-log-count');
3441 -
3442 - $.ajax({
3443 - url: ajaxurl,
3444 - type: 'POST',
3445 - data: {
3446 - action: 'mxchat_get_debug_log',
3447 - _ajax_nonce: mxchatAdmin.setting_nonce
3448 - },
3449 - success: function(response) {
3450 - if (response.success) {
3451 - var log = response.data.log;
3452 - var count = response.data.count;
3453 -
3454 - if (count > 0) {
3455 - $countBadge.text(count + ' entries').show();
3456 - var html = '<div class="mxchat-debug-log-entries">';
3457 - log.forEach(function(entry) {
3458 - var typeClass = 'mxchat-log-type-' + entry.type;
3459 - var typeLabel = entry.type.replace(/_/g, ' ').toUpperCase();
3460 - html += '<div class="mxchat-debug-log-entry ' + typeClass + '">';
3461 - html += '<div class="mxchat-log-header">';
3462 - html += '<span class="mxchat-log-type">' + escapeHtml(typeLabel) + '</span>';
3463 - html += '<span class="mxchat-log-time">' + escapeHtml(entry.time) + '</span>';
3464 - html += '</div>';
3465 - html += '<div class="mxchat-log-message">' + escapeHtml(entry.message) + '</div>';
3466 - if (entry.data) {
3467 - html += '<div class="mxchat-log-data">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</div>';
3468 - }
3469 - html += '</div>';
3470 - });
3471 - html += '</div>';
3472 - $container.html(html);
3473 - } else {
3474 - $countBadge.hide();
3475 - $container.html(
3476 - '<div class="mxchat-debug-log-empty">' +
3477 - '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="opacity: 0.3;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
3478 - '<p>No log entries yet. Enable debug mode to start logging.</p>' +
3479 - '</div>'
3480 - );
3481 - }
3482 - }
3483 - }
3484 - });
3485 - }
3486 -
3487 - // Load debug log on page load
3488 - if ($('#mxchat-debug-log').length) {
3489 - refreshDebugLog();
3490 - }
3491 -
3492 - // Refresh Log Button
3493 - $('#mxchat-refresh-log').on('click', function() {
3494 - var $btn = $(this);
3495 - $btn.prop('disabled', true);
3496 - refreshDebugLog();
3497 - setTimeout(function() {
3498 - $btn.prop('disabled', false);
3499 - }, 500);
3500 - });
3501 -
3502 - // Clear Log Button
3503 - $('#mxchat-clear-log').on('click', function() {
3504 - if (!confirm('Are you sure you want to clear the debug log?')) {
3505 - return;
3506 - }
3507 -
3508 - var $btn = $(this);
3509 - $btn.prop('disabled', true);
3510 -
3511 - $.ajax({
3512 - url: ajaxurl,
3513 - type: 'POST',
3514 - data: {
3515 - action: 'mxchat_clear_debug_log',
3516 - _ajax_nonce: mxchatAdmin.setting_nonce
3517 - },
3518 - success: function(response) {
3519 - if (response.success) {
3520 - refreshDebugLog();
3521 - } else {
3522 - alert(response.data.message || 'Error clearing log');
3523 - }
3524 - },
3525 - error: function() {
3526 - alert('Error clearing log');
3527 - },
3528 - complete: function() {
3529 - $btn.prop('disabled', false);
3530 - }
3531 - });
3532 - });
3533 -
3534 - // Export Settings Button
3535 - $('#mxchat-export-settings').on('click', function() {
3536 - var $btn = $(this);
3537 - var originalHtml = $btn.html();
3538 - $btn.prop('disabled', true).html(
3539 - '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/></svg> Exporting...'
3540 - );
3541 -
3542 - $.ajax({
3543 - url: ajaxurl,
3544 - type: 'POST',
3545 - data: {
3546 - action: 'mxchat_export_settings',
3547 - _ajax_nonce: mxchatAdmin.setting_nonce
3548 - },
3549 - success: function(response) {
3550 - if (response.success) {
3551 - // Create and download the JSON file
3552 - var dataStr = JSON.stringify(response.data.settings, null, 2);
3553 - var blob = new Blob([dataStr], { type: 'application/json' });
3554 - var url = URL.createObjectURL(blob);
3555 - var a = document.createElement('a');
3556 - a.href = url;
3557 - a.download = response.data.filename;
3558 - document.body.appendChild(a);
3559 - a.click();
3560 - document.body.removeChild(a);
3561 - URL.revokeObjectURL(url);
3562 - } else {
3563 - alert(response.data.message || 'Error exporting settings');
3564 - }
3565 - },
3566 - error: function() {
3567 - alert('Error exporting settings');
3568 - },
3569 - complete: function() {
3570 - $btn.prop('disabled', false).html(originalHtml);
3571 - }
3572 - });
3573 - });
3574 -
3575 - // Reset Settings Modal
3576 - var $resetModal = $('#mxchat-reset-modal');
3577 - var $resetConfirmInput = $('#mxchat-reset-confirmation');
3578 - var $resetConfirmBtn = $('#mxchat-reset-confirm');
3579 -
3580 - $('#mxchat-reset-settings').on('click', function() {
3581 - $resetModal.fadeIn(200);
3582 - $resetConfirmInput.val('').focus();
3583 - $resetConfirmBtn.prop('disabled', true);
3584 - });
3585 -
3586 - $('#mxchat-reset-modal-close, #mxchat-reset-cancel, .mxch-modal-backdrop').on('click', function() {
3587 - $resetModal.fadeOut(200);
3588 - });
3589 -
3590 - $resetConfirmInput.on('input', function() {
3591 - var value = $(this).val().toUpperCase();
3592 - $resetConfirmBtn.prop('disabled', value !== 'RESET');
3593 - });
3594 -
3595 - $resetConfirmBtn.on('click', function() {
3596 - var $btn = $(this);
3597 - var confirmation = $resetConfirmInput.val();
3598 -
3599 - $btn.prop('disabled', true).text('Resetting...');
3600 -
3601 - $.ajax({
3602 - url: ajaxurl,
3603 - type: 'POST',
3604 - data: {
3605 - action: 'mxchat_reset_all_settings',
3606 - confirmation: confirmation,
3607 - _ajax_nonce: mxchatAdmin.setting_nonce
3608 - },
3609 - success: function(response) {
3610 - if (response.success) {
3611 - alert(response.data.message);
3612 - window.location.reload();
3613 - } else {
3614 - alert(response.data.message || 'Error resetting settings');
3615 - $btn.prop('disabled', false).text('Reset All Settings');
3616 - }
3617 - },
3618 - error: function() {
3619 - alert('Error resetting settings');
3620 - $btn.prop('disabled', false).text('Reset All Settings');
3621 - }
3622 - });
3623 - });
3624 -
3625 - // Load debug log on page load if we're on the optimization section
3626 - if ($('#optimization').hasClass('active') || window.location.hash === '#optimization') {
3627 - refreshDebugLog();
3628 - }
3629 -
3630 - // Also refresh when switching to optimization tab
3631 - $(document).on('click', '[data-section="optimization"]', function() {
3632 - setTimeout(refreshDebugLog, 100);
3633 - });
3634 -});