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

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

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