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

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

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