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

2,675 lines 120.9 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
64 // Updated mxchatOpenActionModal function to integrate with the new selector
65 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
66 const modal = document.getElementById('mxchat-action-modal');
67 if (!modal) return;
68
69 // Get form fields
70 const actionIdField = document.getElementById('edit_action_id');
71 const labelField = document.getElementById('intent_label');
72 const phrasesField = document.getElementById('action_phrases');
73 const formActionType = document.getElementById('form_action_type');
74 const callbackGroup = document.getElementById('callback_selection_group');
75 const callbackSelect = document.getElementById('callback_function');
76 const saveButton = document.getElementById('mxchat-save-action-btn');
77 const nonceContainer = document.getElementById('action-nonce-container');
78 const thresholdSlider = document.getElementById('similarity_threshold');
79 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
80
81 // Set up modal for edit or create
82 if (isEdit) {
83 saveButton.textContent = 'Update Action';
84 formActionType.value = 'mxchat_edit_intent';
85 actionIdField.value = actionId;
86 labelField.value = label;
87 phrasesField.value = phrases;
88 callbackGroup.style.display = 'none'; // Hide callback selection when editing
89 thresholdSlider.value = threshold; // Set the current threshold value
90 thresholdDisplay.textContent = threshold + '%'; // Update display
91
92 // Remove the required attribute when editing
93 callbackSelect.removeAttribute('required');
94
95 // Update the nonce field for editing
96 nonceContainer.innerHTML = ''; // Clear existing nonce
97 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
98 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
99 }
100 } else {
101 saveButton.textContent = 'Save Action';
102 formActionType.value = 'mxchat_add_intent';
103 actionIdField.value = '';
104 labelField.value = '';
105 phrasesField.value = '';
106 callbackGroup.style.display = 'block'; // Show callback selection when creating
107 thresholdSlider.value = 85; // Default value for new actions
108 thresholdDisplay.textContent = '85%'; // Default display
109
110 // Ensure the required attribute is present when adding
111 callbackSelect.setAttribute('required', 'required');
112
113 // Update the nonce field for adding
114 nonceContainer.innerHTML = ''; // Clear existing nonce
115 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
116 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
117 }
118 }
119
120 // Show modal with animation
121 modal.style.display = 'flex';
122 requestAnimationFrame(() => {
123 modal.classList.add('active');
124 });
125
126 // Set up close handlers
127 const closeModal = () => {
128 modal.classList.remove('active');
129 setTimeout(() => {
130 modal.style.display = 'none';
131 }, 300); // Match the CSS transition time
132 };
133
134 // Close button handler
135 const closeBtn = modal.querySelector('.mxchat-modal-close');
136 if (closeBtn) {
137 closeBtn.onclick = closeModal;
138 }
139
140 // Cancel button handler
141 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
142 if (cancelBtn) {
143 cancelBtn.onclick = closeModal;
144 }
145
146 // Click outside modal to close
147 modal.onclick = (e) => {
148 if (e.target === modal) {
149 closeModal();
150 }
151 };
152
153 // Escape key to close modal
154 document.addEventListener('keydown', function(e) {
155 if (e.key === 'Escape' && modal.classList.contains('active')) {
156 closeModal();
157 }
158 }, { once: true });
159
160 // Focus the first field
161 labelField.focus();
162
163 // Dispatch an event for the action type selector to catch
164 const event = new CustomEvent('mxchatModalOpened', {
165 detail: {
166 isEdit: isEdit,
167 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
168 }
169 });
170 document.dispatchEvent(event);
171
172 return closeModal; // Return close function for external use
173 }
174
175 // Initialize event listeners
176 document.addEventListener('DOMContentLoaded', () => {
177 // Set up edit button handlers for intents
178 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
179 button.onclick = () => {
180 const intentId = button.dataset.intentId;
181 const phrases = button.dataset.phrases;
182 mxchatOpenEditModal(intentId, phrases);
183 };
184 });
185
186 // Set up edit button handlers for actions (new functionality)
187 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
188 button.onclick = () => {
189 const actionId = button.dataset.actionId;
190 const phrases = button.dataset.phrases;
191 const label = button.dataset.label;
192 const threshold = button.dataset.threshold || 85;
193 const callbackFunction = button.dataset.callbackFunction; // Add this data attribute
194 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
195 };
196 });
197
198 // Set up add new action buttons (new functionality)
199 const addActionBtn = document.getElementById('mxchat-add-action-btn');
200 if (addActionBtn) {
201 addActionBtn.onclick = () => mxchatOpenActionModal();
202 }
203
204 const createFirstAction = document.getElementById('mxchat-create-first-action');
205 if (createFirstAction) {
206 createFirstAction.onclick = () => mxchatOpenActionModal();
207 }
208
209 // Setup category-specific new action buttons (new functionality)
210 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
211 button.onclick = () => {
212 const category = button.closest('.mxchat-new-action-card').dataset.category;
213 const closeModal = mxchatOpenActionModal();
214
215 // Pre-select the appropriate callback based on category
216 if (category) {
217 const callbackSelect = document.getElementById('callback_function');
218 if (callbackSelect) {
219 setTimeout(() => {
220 // Map categories to default callbacks
221 const categoryToCallback = {
222 'data_collection': 'mxchat_handle_form_collection',
223 'integrations': 'mxchat_handle_slack_message',
224 'custom_actions': 'mxchat_handle_custom_action',
225 'recommendations': 'mxchat_handle_product_recommendations'
226 // Add more mappings as needed
227 };
228
229 if (categoryToCallback[category]) {
230 callbackSelect.value = categoryToCallback[category];
231 }
232 }, 100);
233 }
234 }
235 };
236 });
237
238 // Handle action toggle switches (new functionality)
239 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
240 toggle.onchange = function() {
241 const actionId = this.dataset.actionId;
242 const isEnabled = this.checked;
243
244 // Show loading indicator
245 const loadingEl = document.getElementById('mxchat-action-loading');
246 if (loadingEl) loadingEl.style.display = 'flex';
247
248 // Send AJAX request to update status
249 fetch(ajaxurl, {
250 method: 'POST',
251 headers: {
252 'Content-Type': 'application/x-www-form-urlencoded',
253 },
254 body: new URLSearchParams({
255 action: 'mxchat_toggle_action',
256 intent_id: actionId,
257 enabled: isEnabled ? 1 : 0,
258 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
259 })
260 })
261 .then(response => response.json())
262 .then(data => {
263 if (!data.success) {
264 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
265 this.checked = !isEnabled; // Revert the toggle
266 }
267 })
268 .catch(error => {
269 //console.error('Error:', error);
270 alert('Server error. Please try again.');
271 this.checked = !isEnabled; // Revert the toggle
272 })
273 .finally(() => {
274 if (loadingEl) loadingEl.style.display = 'none';
275 });
276 };
277 });
278
279 // Handle threshold sliders in action cards (new functionality)
280 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
281 slider.oninput = function() {
282 const actionId = this.id.replace('intent_threshold_', '');
283 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
284 };
285 });
286
287 // Handle threshold save buttons in action cards (new functionality)
288 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
289 button.onclick = function(e) {
290 e.preventDefault();
291 const form = this.closest('form');
292 const intentId = form.querySelector('input[name="intent_id"]').value;
293 const threshold = form.querySelector('input[name="intent_threshold"]').value;
294 const nonce = form.querySelector('input[name="_wpnonce"]').value;
295
296 // Show loading indicator
297 const loadingEl = document.getElementById('mxchat-action-loading');
298 if (loadingEl) loadingEl.style.display = 'flex';
299
300 // Send AJAX request
301 fetch(ajaxurl, {
302 method: 'POST',
303 headers: {
304 'Content-Type': 'application/x-www-form-urlencoded',
305 },
306 body: new URLSearchParams({
307 action: 'mxchat_update_intent_threshold',
308 intent_id: intentId,
309 intent_threshold: threshold,
310 _wpnonce: nonce
311 })
312 })
313 .then(response => response.json())
314 .then(data => {
315 if (data.success) {
316 // Visual feedback of success
317 const card = this.closest('.mxchat-action-card');
318 card.style.background = 'rgba(120, 115, 245, 0.1)';
319 setTimeout(() => {
320 card.style.background = 'white';
321 }, 300);
322 } else {
323 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
324 }
325 })
326 .catch(error => {
327 //console.error('Error:', error);
328 alert('Server error. Please try again.');
329 })
330 .finally(() => {
331 if (loadingEl) loadingEl.style.display = 'none';
332 });
333 };
334 });
335 });
336
337 jQuery(document).ready(function($) {
338 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
339 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
340
341 // --- AJAX Auto-Save ---
342 let $autosaveSections = $('.mxchat-autosave-section');
343
344 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
345 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
346 if ($pineconeAutosaveSection.length) {
347 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
348 console.log('Added Pinecone section to auto-save monitoring');
349 }
350
351 // Track whether fields have been modified by user
352 const userModifiedFields = new Set();
353
354 if ($autosaveSections.length) {
355 // Track user interactions with input fields to determine if changes are user-initiated
356 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
357 const fieldName = $(this).attr('name');
358 if (fieldName) {
359 userModifiedFields.add(fieldName);
360 }
361 });
362
363 // Handle real-time range slider value updates
364 $autosaveSections.find('input[type="range"]').on('input', function() {
365 const value = $(this).val();
366 $('#threshold_value').text(value);
367 });
368
369 // Handle all input changes (including range slider)
370 $autosaveSections.find('input, textarea, select').on('change', function() {
371 const $field = $(this);
372 const name = $field.attr('name');
373
374 // Skip saving for API key fields that haven't been interacted with and are empty
375 const isApiKeyField = name && (
376 name === 'loops_api_key' ||
377 name === 'api_key' ||
378 name === 'xai_api_key' ||
379 name === 'claude_api_key' ||
380 name === 'voyage_api_key' ||
381 name === 'gemini_api_key' ||
382 name === 'deepseek_api_key' ||
383 name.indexOf('_api_key') !== -1
384 );
385
386 // Skip processing if:
387 // 1. It's an API key field
388 // 2. The user hasn't interacted with it
389 // 3. The field is empty
390 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
391 //console.log('Skipping auto-save for untouched API key field:', name);
392 return;
393 }
394
395 let value;
396
397 // Handle different input types
398 if ($field.attr('type') === 'checkbox') {
399 // *** UPDATED: Handle Pinecone checkboxes differently ***
400 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
401 value = $field.is(':checked') ? '1' : '0';
402 } else {
403 value = $field.is(':checked') ? 'on' : 'off';
404 }
405 } else {
406 value = $field.val();
407 }
408
409 // Create feedback container
410 const feedbackContainer = $('<div class="feedback-container"></div>');
411 const spinner = $('<div class="saving-spinner"></div>');
412 const successIcon = $('<div class="success-icon">✔</div>');
413
414 // Position feedback container based on input type
415 if ($field.closest('.toggle-switch').length) {
416 $field.closest('td').append(feedbackContainer);
417 } else if ($field.closest('.mxchat-toggle-switch').length) {
418 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
419 } else if ($field.closest('.slider-container').length) {
420 $field.closest('.slider-container').after(feedbackContainer);
421 } else {
422 $field.after(feedbackContainer);
423 }
424 feedbackContainer.append(spinner);
425
426 // Determine which AJAX action and nonce to use:
427 var ajaxAction, nonce;
428 // *** UPDATED: Add Pinecone fields to prompts action ***
429 if (name.indexOf('mxchat_prompts_options') !== -1 ||
430 name === 'mxchat_auto_sync_posts' ||
431 name === 'mxchat_auto_sync_pages' ||
432 name.indexOf('mxchat_auto_sync_') === 0 ||
433 name.indexOf('mxchat_pinecone_addon_options') !== -1) { // *** ADD THIS LINE ***
434 ajaxAction = 'mxchat_save_prompts_setting';
435 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
436 } else {
437 // Otherwise, use the existing AJAX action.
438 ajaxAction = 'mxchat_save_setting';
439 nonce = mxchatAdmin.setting_nonce;
440 }
441
442 // *** ADD THIS: Debug logging for Pinecone fields ***
443 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
444 console.log('Saving Pinecone field:', name, '=', value);
445 }
446
447 // AJAX save request
448 $.ajax({
449 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
450 type: 'POST',
451 data: {
452 action: ajaxAction,
453 name: name,
454 value: value,
455 _ajax_nonce: nonce
456 },
457 success: function(response) {
458 if (response.success) {
459 spinner.fadeOut(200, function() {
460 feedbackContainer.append(successIcon);
461 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
462 feedbackContainer.remove();
463 });
464 });
465
466 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
467 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
468 console.log('Pinecone toggle saved successfully, value:', value);
469
470 // The checkbox state is already updated by the user interaction
471 // But let's make sure the UI state matches the saved value
472 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
473 var settingsDiv = $('.mxchat-pinecone-settings');
474
475 // Double-check the UI state matches what was saved
476 if (value === '1' && !$checkbox.is(':checked')) {
477 $checkbox.prop('checked', true);
478 settingsDiv.slideDown(300);
479 } else if (value === '0' && $checkbox.is(':checked')) {
480 $checkbox.prop('checked', false);
481 settingsDiv.slideUp(300);
482 }
483
484 console.log('Pinecone UI state synchronized');
485
486 // Check if Knowledge Import tab is currently active
487 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
488 // Show a notice that we need to refresh
489 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
490 if ($knowledgeCard.length > 0) {
491 // Add a refresh notice at the top of the knowledge base card
492 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
493 '<p style="margin: 0;">' +
494 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
495 'Database settings have changed. ' +
496 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
497 '</p></div>');
498
499 $knowledgeCard.prepend(refreshNotice);
500 }
501 } else {
502 // If not on import tab, set a flag to refresh when they go there
503 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
504 }
505 }
506
507 // *** ADD THIS: Debug logging for successful saves ***
508 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
509 console.log('Pinecone field saved successfully:', name, '=', value);
510 }
511
512 // Check if the response contains a "no changes" message and log it
513 if (response.data && response.data.message === 'No changes detected') {
514 //console.log('No changes detected for field:', name);
515 }
516 } else {
517 // Only show alert for actual errors, not for "no changes"
518 let errorMessage = response.data?.message || 'Unknown error';
519
520 // Don't display an alert for "no changes" message
521 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
522 alert('Error saving: ' + errorMessage);
523 } else {
524 // Still provide visual feedback that no changes were needed
525 spinner.fadeOut(200, function() {
526 feedbackContainer.append(successIcon);
527 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
528 feedbackContainer.remove();
529 });
530 });
531 //console.log('No changes detected for field:', name);
532 return;
533 }
534
535 // Only revert checkbox state if it was an actual error
536 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
537 if ($field.attr('type') === 'checkbox') {
538 $field.prop('checked', !$field.is(':checked'));
539 }
540 }
541
542 // Always clean up the feedback container
543 feedbackContainer.remove();
544 }
545 },
546 error: function(xhr, textStatus, error) {
547 //console.error('AJAX Error:', textStatus, error);
548 alert('An error occurred while saving. Please try again.');
549
550 // Revert checkbox state on error
551 if ($field.attr('type') === 'checkbox') {
552 $field.prop('checked', !$field.is(':checked'));
553 }
554
555 feedbackContainer.remove();
556 }
557 });
558 });
559
560 // Initialize color pickers with debouncing
561 $autosaveSections.find('.my-color-field').each(function() {
562 const $colorField = $(this);
563
564 $(this).wpColorPicker({
565 change: useDebounce(function(event, ui) {
566 // Safety check - ensure we have a valid field and value
567 if (!$colorField || !$colorField.val()) {
568 //console.warn('Color picker not ready');
569 return;
570 }
571
572 const name = $colorField.attr('name');
573 const value = $colorField.val();
574
575 if (!name || !value) {
576 //console.warn('Missing required color picker values');
577 return;
578 }
579
580 // Create feedback container
581 const feedbackContainer = $('<div class="feedback-container"></div>');
582 const spinner = $('<div class="saving-spinner"></div>');
583 const successIcon = $('<div class="success-icon">✔</div>');
584
585 // Position feedback container
586 $colorField.closest('.wp-picker-container').after(feedbackContainer);
587 feedbackContainer.append(spinner);
588
589 // Determine which AJAX action and nonce to use:
590 var ajaxAction, nonce;
591 // Use the new AJAX action for submenu fields:
592 if (name.indexOf('mxchat_prompts_options') !== -1 ||
593 name === 'mxchat_auto_sync_posts' ||
594 name === 'mxchat_auto_sync_pages' ||
595 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
596 ajaxAction = 'mxchat_save_prompts_setting';
597 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
598 } else {
599 // Otherwise, use the existing AJAX action.
600 ajaxAction = 'mxchat_save_setting';
601 nonce = mxchatAdmin.setting_nonce;
602 }
603 // AJAX save request
604 $.ajax({
605 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
606 type: 'POST',
607 data: {
608 action: ajaxAction,
609 name: name,
610 value: value,
611 _ajax_nonce: nonce
612 },
613 success: function(response) {
614 if (response.success) {
615 spinner.fadeOut(200, function() {
616 feedbackContainer.append(successIcon);
617 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
618 feedbackContainer.remove();
619 });
620 });
621 } else {
622 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
623 feedbackContainer.remove();
624 }
625 },
626 error: function() {
627 alert('An error occurred while saving.');
628 feedbackContainer.remove();
629 }
630 });
631 }, 500)
632 });
633 });
634
635 // Reinitialize color pickers when switching tabs
636 $('.mxchat-tab-button').on('click.mxchat', function() {
637 setTimeout(function() {
638 $('.my-color-field:visible').wpColorPicker('close');
639 }, 100);
640 });
641 }
642
643 // Initialize tabs system
644 function initTabs() {
645 // Remove any existing handlers first
646 $('.mxchat-tab-button').off('click.mxchat');
647
648 // Add new click handlers
649 $('.mxchat-tab-button').on('click.mxchat', function(e) {
650 e.preventDefault();
651 e.stopPropagation();
652
653 var $this = $(this);
654
655 // Get tab ID from data-tab attribute
656 var tabId = $this.data('tab') || 'chatbot';
657
658 // Safety check for empty tabId
659 if (!tabId) {
660 //console.warn('No tab identifier found');
661 return;
662 }
663
664 // Update tab buttons
665 $('.mxchat-tab-button').removeClass('active');
666 $this.addClass('active');
667
668 // Update content areas - with safety check
669 $('.mxchat-tab-content').removeClass('active');
670 var $targetTab = $('#' + tabId);
671 if ($targetTab.length) {
672 $targetTab.addClass('active');
673 // Removed localStorage saving functionality
674 } else {
675 //console.warn('Tab content #' + tabId + ' not found');
676 }
677 });
678 }
679
680 // Initialize tabs and handle events
681 initTabs();
682 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
683
684 // Always activate the first tab (Chatbot)
685 $('.mxchat-tab-button').first().trigger('click.mxchat');
686
687 // Attach edit modal event handler
688 $(document).on('click', '.mxchat-edit-button', function() {
689 const intentId = $(this).data('intent-id');
690 const phrases = $(this).data('phrases');
691 mxchatOpenEditModal(intentId, phrases);
692 });
693
694 // Toggle visibility handlers
695 function toggleVisibility(selector) {
696 $(selector).on('click', function() {
697 var inputField = $(this).prev('input');
698 if (inputField.attr('type') === 'password') {
699 inputField.attr('type', 'text');
700 $(this).text('Hide');
701 } else {
702 inputField.attr('type', 'password');
703 $(this).text('Show');
704 }
705 });
706 }
707
708 // Initialize all toggle visibility buttons
709 [
710 '#toggleApiKeyVisibility',
711 '#toggleWooCommerceSecretVisibility',
712 '#toggleVoyageAPIKeyVisibility',
713 '#toggleLoopsApiKeyVisibility',
714 '#toggleXaiApiKeyVisibility',
715 '#toggleClaudeApiKeyVisibility',
716 '#toggleBraveApiKeyVisibility',
717 '#toggleWebhookUrlVisibility',
718 '#toggleSecretKeyVisibility',
719 '#toggleBotTokenVisibility',
720 '#toggleDeepSeekApiKeyVisibility',
721 '#toggleGeminiApiKeyVisibility' // Added Gemini toggle
722 ].forEach(toggleVisibility);
723
724 // Handle API key visibility based on model selection
725 function setupAPIKeyVisibility() {
726 // Cache the selectors
727 const $chatModelSelect = $('#model');
728 const $embeddingModelSelect = $('#embedding_model');
729
730 // First, locate and mark the API key rows
731 setupAPIKeyRows();
732
733 // Initial setup based on current selections
734 updateApiKeyVisibility();
735
736 // Listen for changes to the model selectors
737 $chatModelSelect.on('change', updateApiKeyVisibility);
738 $embeddingModelSelect.on('change', updateApiKeyVisibility);
739
740 /**
741 * Locate and mark rows that contain API key fields
742 */
743 function setupAPIKeyRows() {
744 // Find key rows by their field IDs
745 const providerMap = {
746 'api_key': 'openai',
747 'xai_api_key': 'xai',
748 'claude_api_key': 'claude',
749 'deepseek_api_key': 'deepseek',
750 'voyage_api_key': 'voyage',
751 'gemini_api_key': 'gemini' // Added Gemini API key mapping
752 };
753
754 $.each(providerMap, function(fieldId, provider) {
755 const $field = $('#' + fieldId);
756 if ($field.length) {
757 const $row = $field.closest('tr');
758 $row.addClass('mxchat-setting-row');
759 $row.attr('data-provider', provider);
760 }
761 });
762 }
763
764 /**
765 * Updates the visibility of API key fields based on current model selections
766 */
767 function updateApiKeyVisibility() {
768 const chatModel = $chatModelSelect.val();
769 const embeddingModel = $embeddingModelSelect.val();
770
771 // Determine which providers are needed
772 const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
773 const isXAI = chatModel && chatModel.startsWith('grok-');
774 const isClaude = chatModel && chatModel.startsWith('claude-');
775 const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
776 const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection
777
778 const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
779 const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
780 const isGeminiEmbedding = embeddingModel && embeddingModel.startsWith('gemini-embedding-');
781
782 // Update API key visibility for each provider
783 updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
784 updateWrapperVisibility('xai', isXAI);
785 updateWrapperVisibility('claude', isClaude);
786 updateWrapperVisibility('deepseek', isDeepSeek);
787 updateWrapperVisibility('voyage', isVoyage);
788 updateWrapperVisibility('gemini', isGemini || isGeminiEmbedding); // Updated Gemini visibility for both chat and embedding
789
790 // Update provider-specific notices for OpenAI
791 if (isOpenAIChat && isOpenAIEmbedding) {
792 $('div[data-provider="openai"] .api-key-notice').text(
793 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
794 );
795 } else if (isOpenAIChat) {
796 $('div[data-provider="openai"] .api-key-notice').text(
797 'Required for your selected chat model. Important: You must add credits before use.'
798 );
799 } else if (isOpenAIEmbedding) {
800 $('div[data-provider="openai"] .api-key-notice').text(
801 'Required for your selected embedding model. Important: You must add credits before use.'
802 );
803 }
804
805 // Update provider-specific notices for Gemini
806 if (isGemini && isGeminiEmbedding) {
807 $('div[data-provider="gemini"] .api-key-notice').text(
808 'Required for your selected chat model and embedding model.'
809 );
810 } else if (isGemini) {
811 $('div[data-provider="gemini"] .api-key-notice').text(
812 'Required for your selected chat model.'
813 );
814 } else if (isGeminiEmbedding) {
815 $('div[data-provider="gemini"] .api-key-notice').text(
816 'Required for your selected embedding model.'
817 );
818 }
819 }
820
821 /**
822 * Updates visibility of a specific provider's API key wrapper
823 */
824 function updateWrapperVisibility(provider, isVisible) {
825 const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
826
827 if (!$row.length) {
828 //console.warn('API key row not found for provider: ' + provider);
829 return;
830 }
831
832 if (isVisible) {
833 $row.show();
834 if (!$row.hasClass('highlighted')) {
835 $row.addClass('highlighted');
836 setTimeout(() => {
837 $row.removeClass('highlighted');
838 }, 1500);
839 }
840 } else {
841 $row.hide();
842 }
843 }
844 }
845
846 // Add this to your JavaScript file
847 function setupMxChatModelSelector() {
848 const $modelSelect = $('#model');
849 const $modelSelectorButton = $('<button>', {
850 type: 'button',
851 id: 'mxchat_model_selector_btn',
852 class: 'button-primary mxchat-model-selector-btn',
853 text: 'Select AI Model'
854 });
855
856 // Replace the select dropdown with a button
857 $modelSelect.hide().after($modelSelectorButton);
858
859 // Update button text to show currently selected model
860 function updateButtonText() {
861 const selectedModel = $modelSelect.val();
862 const selectedModelText = $modelSelect.find('option:selected').text();
863 $modelSelectorButton.text(selectedModelText);
864 }
865
866 // Initialize button text
867 updateButtonText();
868
869 // Create and append modal HTML
870 const modelSelectorModal = `
871 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
872 <div class="mxchat-model-selector-modal-content">
873 <div class="mxchat-model-selector-modal-header">
874 <h3>Select AI Model</h3>
875 <span class="mxchat-model-selector-modal-close">&times;</span>
876 </div>
877 <div class="mxchat-model-selector-modal-body">
878 <div class="mxchat-model-selector-search-container">
879 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
880 </div>
881 <div class="mxchat-model-selector-categories">
882 <button class="mxchat-model-category-btn active" data-category="all">All</button>
883 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
884 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
885 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
886 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
887 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
888 </div>
889 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
890 </div>
891 <div class="mxchat-model-selector-modal-footer">
892 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
893 </div>
894 </div>
895 </div>
896 `;
897
898 $('body').append(modelSelectorModal);
899
900 // Populate models grid
901 function populateModelsGrid(filter = '', category = 'all') {
902 const $grid = $('#mxchat_models_grid');
903 $grid.empty();
904
905 const models = {
906 gemini: [
907 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
908 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
909 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
910 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
911 ],
912 openai: [
913 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
914 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
915 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
916 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
917 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
918 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
919 ],
920 claude: [
921 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Most capable Claude model' },
922 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
923 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'High intelligence' },
924 { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
925 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
926 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
927 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
928 ],
929 xai: [
930 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
931 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
932 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
933 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
934 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
935 ],
936 deepseek: [
937 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
938 ],
939 };
940
941 let allModels = [];
942 Object.keys(models).forEach(key => {
943 if (category === 'all' || category === key) {
944 allModels = allModels.concat(models[key]);
945 }
946 });
947
948 // Filter by search term if present
949 if (filter) {
950 const lowerFilter = filter.toLowerCase();
951 allModels = allModels.filter(model =>
952 model.label.toLowerCase().includes(lowerFilter) ||
953 model.description.toLowerCase().includes(lowerFilter)
954 );
955 }
956
957 // Create model cards
958 allModels.forEach(model => {
959 const isSelected = $modelSelect.val() === model.value;
960 const $modelCard = $(`
961 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
962 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
963 <div class="mxchat-model-selector-info">
964 <h4 class="mxchat-model-selector-title">${model.label}</h4>
965 <p class="mxchat-model-selector-description">${model.description}</p>
966 </div>
967 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
968 </div>
969 `);
970 $grid.append($modelCard);
971 });
972 }
973
974 // Helper function to get icon for each model
975 function getModelIcon(modelValue) {
976 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>';
977 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>';
978 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>';
979 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>';
980 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>';
981 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
982 }
983
984 // Event handlers
985 $modelSelectorButton.on('click', function() {
986 $('#mxchat_model_selector_modal').show();
987 populateModelsGrid('', 'all');
988 });
989
990 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
991 $('#mxchat_model_selector_modal').hide();
992 });
993
994 $('.mxchat-model-category-btn').on('click', function() {
995 $('.mxchat-model-category-btn').removeClass('active');
996 $(this).addClass('active');
997 const category = $(this).data('category');
998 const searchTerm = $('#mxchat_model_search_input').val();
999 populateModelsGrid(searchTerm, category);
1000 });
1001
1002 $('#mxchat_model_search_input').on('input', function() {
1003 const searchTerm = $(this).val();
1004 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1005 populateModelsGrid(searchTerm, activeCategory);
1006 });
1007
1008 $(document).on('click', '.mxchat-model-selector-card', function() {
1009 const modelValue = $(this).data('value');
1010 $modelSelect.val(modelValue).trigger('change');
1011 updateButtonText();
1012 $('#mxchat_model_selector_modal').hide();
1013 });
1014
1015 // Close modal when clicking outside
1016 $(window).on('click', function(event) {
1017 if ($(event.target).is('#mxchat_model_selector_modal')) {
1018 $('#mxchat_model_selector_modal').hide();
1019 }
1020 });
1021 }
1022
1023 // Embedding model selector - completely separate from chat model selector
1024 function setupMxChatEmbeddingModelSelector() {
1025 const $embeddingModelSelect = $('#embedding_model');
1026
1027 // Skip if the element doesn't exist on the page
1028 if ($embeddingModelSelect.length === 0) {
1029 return;
1030 }
1031
1032 const $embeddingModelSelectorButton = $('<button>', {
1033 type: 'button',
1034 id: 'mxchat_embedding_model_selector_btn',
1035 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1036 text: 'Select Embedding Model'
1037 });
1038
1039 // Replace the select dropdown with a button
1040 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1041
1042 // Update button text to show currently selected model
1043 function updateButtonText() {
1044 const selectedModel = $embeddingModelSelect.val();
1045 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1046 $embeddingModelSelectorButton.text(selectedModelText);
1047 }
1048
1049 // Initialize button text
1050 updateButtonText();
1051
1052 // Create a unique ID for the modal to avoid conflicts
1053 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1054
1055 // Create and append modal HTML with unique IDs
1056 const embeddingModelSelectorModal = `
1057 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1058 <div class="mxchat-embedding-model-selector-modal-content">
1059 <div class="mxchat-embedding-model-selector-modal-header">
1060 <h3>Select Embedding Model</h3>
1061 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1062 </div>
1063 <div class="mxchat-embedding-model-selector-modal-body">
1064 <div class="mxchat-embedding-model-selector-search-container">
1065 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1066 </div>
1067 <div class="mxchat-embedding-model-selector-categories">
1068 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1069 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1070 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1071 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1072 </div>
1073 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1074 </div>
1075 <div class="mxchat-embedding-model-selector-modal-footer">
1076 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1077 </div>
1078 </div>
1079 </div>
1080 `;
1081
1082 // Use jQuery's append to ensure it doesn't clash with existing modals
1083 $('body').append(embeddingModelSelectorModal);
1084
1085 // Populate models grid
1086 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1087 const $grid = $('#mxchat_embedding_models_grid');
1088 $grid.empty();
1089
1090 // Define embedding models with descriptions and context lengths
1091 const models = {
1092 openai: [
1093 {
1094 value: 'text-embedding-3-small',
1095 label: 'TE3 Small',
1096 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1097 },
1098 {
1099 value: 'text-embedding-ada-002',
1100 label: 'Ada 2',
1101 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1102 },
1103 {
1104 value: 'text-embedding-3-large',
1105 label: 'TE3 Large',
1106 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1107 }
1108 ],
1109 voyage: [
1110 {
1111 value: 'voyage-3-large',
1112 label: 'Voyage-3 Large',
1113 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1114 }
1115 ],
1116 gemini: [
1117 {
1118 value: 'gemini-embedding-exp-03-07',
1119 label: 'Gemini Embedding',
1120 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1121 }
1122 ]
1123 };
1124
1125 let allModels = [];
1126 Object.keys(models).forEach(key => {
1127 if (category === 'all' || category === key) {
1128 allModels = allModels.concat(models[key]);
1129 }
1130 });
1131
1132 // Filter by search term if present
1133 if (filter) {
1134 const lowerFilter = filter.toLowerCase();
1135 allModels = allModels.filter(model =>
1136 model.label.toLowerCase().includes(lowerFilter) ||
1137 model.description.toLowerCase().includes(lowerFilter)
1138 );
1139 }
1140
1141 // Create model cards
1142 allModels.forEach(model => {
1143 const isSelected = $embeddingModelSelect.val() === model.value;
1144 let providerClass = 'mxchat-embedding-model-provider-openai';
1145
1146 if (model.value.startsWith('voyage-')) {
1147 providerClass = 'mxchat-embedding-model-provider-voyage';
1148 } else if (model.value.startsWith('gemini-embedding-')) {
1149 providerClass = 'mxchat-embedding-model-provider-gemini';
1150 }
1151
1152 let iconHTML = '';
1153 if (model.value.startsWith('voyage-')) {
1154 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1155 } else if (model.value.startsWith('gemini-embedding-')) {
1156 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1157 } else {
1158 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>';
1159 }
1160
1161 const $modelCard = $(`
1162 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1163 <div class="mxchat-embedding-model-selector-icon">
1164 ${iconHTML}
1165 </div>
1166 <div class="mxchat-embedding-model-selector-info">
1167 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1168 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1169 </div>
1170 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1171 </div>
1172 `);
1173
1174 $grid.append($modelCard);
1175 });
1176 }
1177
1178 // Event handlers - use namespaced events to avoid conflicts
1179 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1180 e.stopPropagation(); // Prevent event bubbling
1181 $('#' + embeddingModalId).show();
1182 populateEmbeddingModelsGrid('', 'all');
1183 });
1184
1185 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1186 e.stopPropagation(); // Prevent event bubbling
1187 $('#' + embeddingModalId).hide();
1188 });
1189
1190 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1191 e.stopPropagation(); // Prevent event bubbling
1192 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1193 $(this).addClass('active');
1194 const category = $(this).data('category');
1195 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1196 populateEmbeddingModelsGrid(searchTerm, category);
1197 });
1198
1199 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1200 const searchTerm = $(this).val();
1201 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1202 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1203 });
1204
1205 // Use a direct selector to avoid conflicts with other card elements
1206 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1207 e.stopPropagation(); // Prevent event bubbling
1208 const modelValue = $(this).data('value');
1209
1210 // Important: Only update this specific select element
1211 $embeddingModelSelect.val(modelValue);
1212
1213 // Manually trigger change only on this element
1214 const changeEvent = new Event('change', { bubbles: true });
1215 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1216
1217 // Update button text
1218 updateButtonText();
1219
1220 // Hide modal
1221 $('#' + embeddingModalId).hide();
1222 });
1223
1224 // Close modal when clicking outside - use namespaced events
1225 $(window).on('click.embeddingModelSelector', function(event) {
1226 if ($(event.target).is('#' + embeddingModalId)) {
1227 $('#' + embeddingModalId).hide();
1228 }
1229 });
1230 }
1231
1232 // Call this function after the DOM is fully loaded
1233 $(document).ready(function() {
1234 setupMxChatModelSelector();
1235 setupMxChatEmbeddingModelSelector();
1236 });
1237
1238 // Initialize API key visibility
1239 setupAPIKeyVisibility();
1240
1241 // Add Intent Form Submission
1242 $('#mxchat-add-intent-form').on('submit', function(event) {
1243 $('#mxchat-intent-loading').show();
1244 $('#mxchat-intent-loading-text').show();
1245 $(this).find('button[type="submit"]').hide();
1246 });
1247
1248 // Inline Edit Functionality
1249 $('.edit-button').on('click', function() {
1250 var row = $(this).closest('tr');
1251 row.find('.content-view, .url-view').hide();
1252 row.find('.content-edit, .url-edit').show();
1253 row.find('.edit-button').hide();
1254 row.find('.save-button').show();
1255 });
1256
1257 // Save button handler
1258 $('.save-button').on('click', function() {
1259 var button = $(this);
1260 var row = button.closest('tr');
1261 var id = button.data('id');
1262 var newContent = row.find('.content-edit').val();
1263 var newUrl = row.find('.url-edit').val();
1264
1265 button.prop('disabled', true);
1266 button.text('Saving...');
1267
1268 $.ajax({
1269 url: mxchatAdmin.ajax_url,
1270 type: 'POST',
1271 data: {
1272 action: 'mxchat_save_inline_prompt',
1273 id: id,
1274 article_content: newContent,
1275 article_url: newUrl,
1276 _ajax_nonce: mxchatAdmin.inline_edit_nonce
1277 },
1278 success: function(response) {
1279 button.prop('disabled', false);
1280 button.text('Save');
1281
1282 if (response.success) {
1283 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1284 if (newUrl) {
1285 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1286 } else {
1287 row.find('.url-view').html('N/A');
1288 }
1289
1290 row.find('.content-edit, .url-edit').hide();
1291 row.find('.content-view, .url-view').show();
1292 row.find('.save-button').hide();
1293 row.find('.edit-button').show();
1294 } else {
1295 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1296 }
1297 },
1298 error: function() {
1299 button.prop('disabled', false);
1300 button.text('Save');
1301 alert('An error occurred while saving.');
1302 }
1303 });
1304 });
1305
1306
1307 // Questions handling
1308 $('.mxchat-add-question').on('click', function () {
1309 const container = $('#mxchat-additional-questions-container');
1310 const questionCount = container.find('.mxchat-question-row').length + 4;
1311 const questionIndex = container.find('.mxchat-question-row').length;
1312
1313 const newQuestion = `
1314 <div class="mxchat-question-row">
1315 <input type="text"
1316 name="additional_popular_questions[]"
1317 placeholder="Enter Additional Popular Question ${questionCount}"
1318 class="regular-text mxchat-question-input"
1319 data-question-index="${questionIndex}" />
1320 <button type="button" class="button mxchat-remove-question"
1321 aria-label="Remove question">Remove</button>
1322 </div>
1323 `;
1324 container.append(newQuestion);
1325 });
1326
1327 $(document).on('click', '.mxchat-remove-question', function () {
1328 $(this).closest('.mxchat-question-row').remove();
1329 saveQuestions();
1330 });
1331
1332 $(document).on('change', '.mxchat-question-input', function() {
1333 saveQuestions();
1334 });
1335
1336 function saveQuestions() {
1337 const questions = [];
1338 $('.mxchat-question-input').each(function() {
1339 const value = $(this).val().trim();
1340 if (value) {
1341 questions.push(value);
1342 }
1343 });
1344
1345 const feedbackContainer = $('<div class="feedback-container"></div>');
1346 const spinner = $('<div class="saving-spinner"></div>');
1347 const successIcon = $('<div class="success-icon">✔</div>');
1348
1349 // Append feedback after the add button
1350 $('.mxchat-add-question').after(feedbackContainer);
1351 feedbackContainer.append(spinner);
1352
1353 // Save via AJAX
1354 $.ajax({
1355 url: mxchatAdmin.ajax_url,
1356 type: 'POST',
1357 data: {
1358 action: 'mxchat_save_setting',
1359 name: 'additional_popular_questions',
1360 value: JSON.stringify(questions),
1361 _ajax_nonce: mxchatAdmin.setting_nonce
1362 },
1363 success: function(response) {
1364 if (response.success) {
1365 spinner.fadeOut(200, function() {
1366 feedbackContainer.append(successIcon);
1367 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1368 feedbackContainer.remove();
1369 });
1370 });
1371 } else {
1372 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1373 feedbackContainer.remove();
1374 }
1375 },
1376 error: function() {
1377 alert('An error occurred while saving questions.');
1378 feedbackContainer.remove();
1379 }
1380 });
1381 }
1382
1383 // Live agent status handler
1384 const statusToggle = document.getElementById('live_agent_status');
1385 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1386 if (statusToggle && statusText) {
1387 statusToggle.addEventListener('change', function() {
1388 // Update display text
1389 statusText.textContent = this.checked ? 'Online' : 'Offline';
1390
1391 // Send the correct on/off value to the server
1392 if (window.mxchatSaveSetting) {
1393 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1394 }
1395 });
1396 }
1397
1398 // Function to adjust the textarea height to content
1399 function adjustTextareaHeight() {
1400 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1401 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1402 }
1403
1404 // Function to reset the textarea height to initial
1405 function resetTextareaHeight() {
1406 this.style.height = ''; // Remove inline height, reverting to CSS default
1407 }
1408
1409 // Target the specific textarea by ID
1410 var $textarea = $('#system_prompt_instructions');
1411
1412 // Bind events
1413 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1414 .on('blur', resetTextareaHeight); // Reset on blur
1415 });
1416
1417
1418
1419 document.addEventListener('DOMContentLoaded', function() {
1420 // Check if we're on the correct page before initializing
1421 const modal = document.getElementById('mxchat-action-modal');
1422
1423 // Only initialize if the modal exists on this page
1424 if (modal) {
1425 //console.log('MXChat Action Modal JS Loaded');
1426
1427 // Initialize the action modal functionality
1428 initStepBasedActionModal();
1429 }
1430
1431 // Function to initialize the step-based action modal
1432 function initStepBasedActionModal() {
1433 // We already checked for modal existence above, so no need to check again
1434
1435 const actionStep1 = document.getElementById('mxchat-action-step-1');
1436 const actionStep2 = document.getElementById('mxchat-action-step-2');
1437 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1438 const searchInput = document.getElementById('action-type-search');
1439 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1440 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1441 const actionForm = document.getElementById('mxchat-action-form');
1442 const callbackInput = document.getElementById('callback_function');
1443 const actionIdField = document.getElementById('edit_action_id');
1444 const labelField = document.getElementById('intent_label');
1445 const phrasesField = document.getElementById('action_phrases');
1446 const formActionType = document.getElementById('form_action_type');
1447 const nonceContainer = document.getElementById('action-nonce-container');
1448 const thresholdSlider = document.getElementById('similarity_threshold');
1449 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1450
1451 // Rest of your initialization code remains the same...
1452
1453 // Log the structure of one action card for debugging
1454 if (actionCards.length > 0) {
1455 //console.log('First action card data attributes:', actionCards[0].dataset);
1456 //console.log('First action card HTML:', actionCards[0].outerHTML);
1457 }
1458
1459 // Add click event listeners to category buttons
1460 categoryButtons.forEach(button => {
1461 button.addEventListener('click', function() {
1462 //console.log('Category button clicked:', this.dataset.category);
1463
1464 // Remove active class from all buttons
1465 categoryButtons.forEach(btn => btn.classList.remove('active'));
1466
1467 // Add active class to clicked button
1468 this.classList.add('active');
1469
1470 // Get selected category
1471 const category = this.dataset.category;
1472
1473 // Filter action cards
1474 filterActionCards(category, searchInput.value);
1475 });
1476 });
1477
1478 // Add search functionality
1479 if (searchInput) {
1480 searchInput.addEventListener('input', function() {
1481 // Get active category
1482 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1483 //console.log('Search input changed, active category:', activeCategory);
1484
1485 // Filter action cards
1486 filterActionCards(activeCategory, this.value);
1487 });
1488 }
1489
1490 // Add click event listeners to action cards
1491 actionCards.forEach(card => {
1492 card.addEventListener('click', function() {
1493 // Get the action data
1494 const isPro = this.dataset.pro === 'true';
1495 const isInstalled = this.dataset.installed === 'true';
1496 const addonName = this.dataset.addon || '';
1497 const actionValue = this.dataset.value;
1498 const actionLabel = this.dataset.label;
1499 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1500 const actionDescription = this.querySelector('p').textContent;
1501
1502 // Pro check using the proper detection method
1503 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1504 (mxchatAdmin.is_activated === '1' ||
1505 mxchatAdmin.is_activated === 'true' ||
1506 mxchatAdmin.is_activated === true);
1507
1508 // Handle different states
1509 if (isPro && !proIsActivated) {
1510 // Pro feature but no Pro license
1511 showProFeatureNotice();
1512 return;
1513 }
1514
1515 if (addonName && !isInstalled) {
1516 // Add-on required but not installed
1517 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1518 showAddonRequiredNotice(addonDisplayName);
1519 return;
1520 }
1521
1522 // If we get here, the action is available - proceed as normal
1523 callbackInput.value = actionValue;
1524
1525 // Update the selected action display in step 2
1526 document.getElementById('selected-action-title').textContent = actionLabel;
1527 document.getElementById('selected-action-description').textContent = actionDescription;
1528 document.getElementById('selected-action-icon').innerHTML =
1529 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1530
1531 // Set a default label based on the action type (user can change it)
1532 if (!labelField.value) {
1533 labelField.value = actionLabel;
1534 }
1535
1536 // Move to step 2
1537 actionStep1.classList.remove('active');
1538 actionStep2.classList.add('active');
1539
1540 // Update modal title
1541 });
1542 });
1543
1544 // Back button functionality
1545 if (backToStep1Btn) {
1546 backToStep1Btn.addEventListener('click', function() {
1547 //console.log('Back button clicked');
1548 actionStep2.classList.remove('active');
1549 actionStep1.classList.add('active');
1550 });
1551 }
1552
1553 // Function to filter action cards by category and search term
1554 function filterActionCards(category, searchTerm) {
1555 searchTerm = searchTerm.toLowerCase().trim();
1556 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1557
1558 let visibleCount = 0;
1559
1560 // Show all cards initially with animation
1561 actionCards.forEach((card, index) => {
1562 // Reset animation
1563 card.style.animation = 'none';
1564 // Trigger reflow
1565 void card.offsetWidth;
1566
1567 // Determine if card should be visible based on category and search term
1568 const cardCategory = card.dataset.category || '';
1569 const matchesCategory = category === 'all' || cardCategory === category;
1570
1571 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1572 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1573 const matchesSearch = searchTerm === '' ||
1574 cardTitle.includes(searchTerm) ||
1575 cardDesc.includes(searchTerm);
1576
1577 // Show/hide card with animation
1578 if (matchesCategory && matchesSearch) {
1579 card.style.display = 'flex';
1580 // Staggered animation for cards
1581 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1582 visibleCount++;
1583 } else {
1584 card.style.display = 'none';
1585 }
1586 });
1587
1588 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1589 }
1590
1591 // Function to show notice for Pro features
1592 function showProFeatureNotice() {
1593 //console.log('Showing Pro feature notice');
1594 // Check if we already have a notification container
1595 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1596
1597 if (!noticeContainer) {
1598 // Create the notice container
1599 noticeContainer = document.createElement('div');
1600 noticeContainer.className = 'mxchat-pro-notice';
1601
1602 // Create content
1603 noticeContainer.innerHTML = `
1604 <div class="mxchat-pro-notice-content">
1605 <h3>MxChat Pro Feature</h3>
1606 <p>This action is available in the Pro version only.</p>
1607 <div class="mxchat-pro-notice-buttons">
1608 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1609 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1610 </div>
1611 </div>
1612 `;
1613
1614 // Append to body
1615 document.body.appendChild(noticeContainer);
1616
1617 // Add close functionality
1618 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1619 closeButton.addEventListener('click', function() {
1620 noticeContainer.classList.remove('active');
1621 setTimeout(() => {
1622 noticeContainer.remove();
1623 }, 300);
1624 });
1625
1626 // Click outside to close
1627 noticeContainer.addEventListener('click', function(e) {
1628 if (e.target === noticeContainer) {
1629 closeButton.click();
1630 }
1631 });
1632
1633 // Show with animation
1634 setTimeout(() => {
1635 noticeContainer.classList.add('active');
1636 }, 10);
1637 } else {
1638 // If it already exists, just make it visible again
1639 noticeContainer.classList.add('active');
1640 }
1641 }
1642
1643 // Function to show notice for add-on requirements
1644 function showAddonRequiredNotice(addonName) {
1645 //console.log(`Showing add-on notice for: ${addonName}`);
1646 // Check if we already have a notification container
1647 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1648
1649 if (!noticeContainer) {
1650 // Create the notice container
1651 noticeContainer = document.createElement('div');
1652 noticeContainer.className = 'mxchat-addon-notice';
1653
1654 // Create content
1655 noticeContainer.innerHTML = `
1656 <div class="mxchat-addon-notice-content">
1657 <span class="mxchat-addon-notice-icon">🧩</span>
1658 <h3>Add-on Required</h3>
1659 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1660 <div class="mxchat-addon-notice-buttons">
1661 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1662 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1663 </div>
1664 </div>
1665 `;
1666
1667 // Append to body
1668 document.body.appendChild(noticeContainer);
1669
1670 // Add close functionality
1671 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1672 closeButton.addEventListener('click', function() {
1673 noticeContainer.classList.remove('active');
1674 setTimeout(() => {
1675 noticeContainer.remove();
1676 }, 300);
1677 });
1678
1679 // Click outside to close
1680 noticeContainer.addEventListener('click', function(e) {
1681 if (e.target === noticeContainer) {
1682 closeButton.click();
1683 }
1684 });
1685
1686 // Show with animation
1687 setTimeout(() => {
1688 noticeContainer.classList.add('active');
1689 }, 10);
1690 } else {
1691 // If it already exists, update the content
1692 const addonNameElement = noticeContainer.querySelector('p strong');
1693 if (addonNameElement) {
1694 addonNameElement.textContent = addonName;
1695 }
1696
1697 // Make it visible again
1698 noticeContainer.classList.add('active');
1699 }
1700 }
1701
1702 // Form submission handling
1703 if (actionForm) {
1704 actionForm.addEventListener('submit', function() {
1705 //console.log('Form submitted');
1706 document.getElementById('mxchat-action-loading').style.display = 'flex';
1707 this.querySelector('button[type="submit"]').disabled = true;
1708 });
1709 }
1710 }
1711
1712 // Setup add action buttons (only if we're on the correct page)
1713 if (modal) {
1714 // Update the modal open function to support the step-based flow
1715 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1716 //console.log('Modal opening, edit mode:', isEdit);
1717
1718 // No need to check again, we already verified modal exists
1719
1720 // Get form fields
1721 const actionIdField = document.getElementById('edit_action_id');
1722 const labelField = document.getElementById('intent_label');
1723 const phrasesField = document.getElementById('action_phrases');
1724 const formActionType = document.getElementById('form_action_type');
1725 const callbackInput = document.getElementById('callback_function');
1726 const saveButton = document.getElementById('mxchat-save-action-btn');
1727 const nonceContainer = document.getElementById('action-nonce-container');
1728 const thresholdSlider = document.getElementById('similarity_threshold');
1729 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1730 const actionStep1 = document.getElementById('mxchat-action-step-1');
1731 const actionStep2 = document.getElementById('mxchat-action-step-2');
1732 const searchInput = document.getElementById('action-type-search');
1733
1734 // Set up modal for edit or create
1735 if (isEdit) {
1736 saveButton.textContent = 'Update Action';
1737 formActionType.value = 'mxchat_edit_intent';
1738 actionIdField.value = actionId;
1739 labelField.value = label;
1740 phrasesField.value = phrases;
1741 callbackInput.value = callbackFunction;
1742 thresholdSlider.value = threshold; // Set the current threshold value
1743 thresholdDisplay.textContent = threshold + '%'; // Update display
1744
1745 // Update the nonce field for editing
1746 nonceContainer.innerHTML = ''; // Clear existing nonce
1747 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1748 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1749 }
1750
1751 // For editing, go directly to step 2 and update the selected action display
1752 actionStep1.classList.remove('active');
1753 actionStep2.classList.add('active');
1754
1755 // Find the matching action card to get its details
1756 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1757 let foundCard = null;
1758
1759 actionCards.forEach(card => {
1760 if (card.dataset.value === callbackFunction) {
1761 foundCard = card;
1762 }
1763 });
1764
1765 if (foundCard) {
1766 //console.log('Found matching action card for:', callbackFunction);
1767 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1768 const actionIconElement = foundCard.querySelector('.dashicons');
1769 const actionIcon = actionIconElement
1770 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1771 : 'admin-generic';
1772 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1773
1774 document.getElementById('selected-action-title').textContent = actionLabel;
1775 document.getElementById('selected-action-description').textContent = actionDescription;
1776 document.getElementById('selected-action-icon').innerHTML =
1777 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1778 } else {
1779 //console.log('No matching action card found for:', callbackFunction);
1780 // Fallback if we can't find the card
1781 document.getElementById('selected-action-title').textContent = label;
1782 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1783 document.getElementById('selected-action-icon').innerHTML =
1784 `<span class="dashicons dashicons-admin-generic"></span>`;
1785 }
1786 } else {
1787 //console.log('Setting up create mode');
1788 saveButton.textContent = 'Save Action';
1789 formActionType.value = 'mxchat_add_intent';
1790 actionIdField.value = '';
1791 labelField.value = '';
1792 phrasesField.value = '';
1793 callbackInput.value = '';
1794 thresholdSlider.value = 85; // Default value for new actions
1795 thresholdDisplay.textContent = '85%'; // Default display
1796
1797 // Update the nonce field for adding
1798 nonceContainer.innerHTML = ''; // Clear existing nonce
1799 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1800 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1801 }
1802
1803 // For creating new, start at step 1
1804 actionStep1.classList.add('active');
1805 actionStep2.classList.remove('active');
1806 }
1807
1808 // Show modal with animation
1809 modal.style.display = 'flex';
1810 requestAnimationFrame(() => {
1811 modal.classList.add('active');
1812 });
1813
1814 // Set up close handlers
1815 const closeModal = () => {
1816 //console.log('Closing modal');
1817 modal.classList.remove('active');
1818 setTimeout(() => {
1819 modal.style.display = 'none';
1820 }, 300); // Match the CSS transition time
1821 };
1822
1823 // Close button handler
1824 const closeBtn = modal.querySelector('.mxchat-modal-close');
1825 if (closeBtn) {
1826 closeBtn.onclick = closeModal;
1827 }
1828
1829 // Cancel button handler
1830 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1831 if (cancelBtns) {
1832 cancelBtns.forEach(btn => {
1833 btn.onclick = closeModal;
1834 });
1835 }
1836
1837 // Click outside modal to close
1838 modal.onclick = (e) => {
1839 if (e.target === modal) {
1840 closeModal();
1841 }
1842 };
1843
1844 // Escape key to close modal
1845 document.addEventListener('keydown', function(e) {
1846 if (e.key === 'Escape' && modal.classList.contains('active')) {
1847 closeModal();
1848 }
1849 }, { once: true });
1850
1851 // Focus appropriate field based on current step
1852 if (isEdit || actionStep2.classList.contains('active')) {
1853 if (labelField) labelField.focus();
1854 } else {
1855 if (searchInput) searchInput.focus();
1856 }
1857
1858 return closeModal; // Return close function for external use
1859 };
1860
1861 // Setup add action buttons
1862 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1863 if (addActionBtn) {
1864 //console.log('Add action button found');
1865 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1866 }
1867
1868 const createFirstAction = document.getElementById('mxchat-create-first-action');
1869 if (createFirstAction) {
1870 //console.log('Create first action button found');
1871 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1872 }
1873
1874 // Setup edit buttons
1875 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1876 //console.log('Edit buttons found:', editButtons.length);
1877 editButtons.forEach(button => {
1878 button.onclick = () => {
1879 const actionId = button.dataset.actionId;
1880 const phrases = button.dataset.phrases;
1881 const label = button.dataset.label;
1882 const threshold = button.dataset.threshold || 85;
1883 const callbackFunction = button.dataset.callbackFunction;
1884
1885 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1886 };
1887 });
1888 }
1889 });
1890
1891 jQuery(document).ready(function($) {
1892 // Toggle custom post types container
1893 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1894 e.preventDefault();
1895
1896 $('#mxchat-custom-post-types-container').slideToggle(300);
1897
1898 // Rotate the toggle icon
1899 const $icon = $(this).find('.mxchat-accordion-icon');
1900 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1901 $icon.css('transform', 'rotate(180deg)');
1902 $(this).closest('.mxchat-settings-accordion').addClass('active');
1903 } else {
1904 $icon.css('transform', 'rotate(0deg)');
1905 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1906 }
1907 });
1908
1909 // If there are any selections made, auto-expand the container
1910 function autoExpandIfNeeded() {
1911 // Check if any checkbox in the container is checked
1912 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1913
1914 if (hasCheckedItems) {
1915 $('#mxchat-custom-post-types-container').show();
1916 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1917 $('.mxchat-settings-accordion').addClass('active');
1918 }
1919 }
1920
1921 // Run on page load
1922 autoExpandIfNeeded();
1923 });
1924
1925 jQuery(document).ready(function($) {
1926 // Track if a form has been submitted to trigger updates
1927 let formSubmitted = false;
1928
1929 // Global interval ID to manage the polling
1930 let updateIntervalId = null;
1931
1932 // Check if we're on the right admin page with status cards or import forms
1933 if ($('.mxchat-status-card').length > 0 || $('.mxchat-import-options').length > 0) {
1934 //console.log('MxChat: Status update script initialized');
1935 // Initialize AJAX status updates
1936 initStatusUpdates();
1937 }
1938
1939 // Initialize status updates
1940 // Initialize status updates
1941 function initStatusUpdates() {
1942 // Get the refresh interval (default to 2 seconds for more responsive updates)
1943 const refreshInterval = parseInt(mxchatAdmin.status_refresh_interval || 2000);
1944
1945 // Check if there are active status cards
1946 const hasActiveStatus = $('.mxchat-status-card').length > 0;
1947
1948 // Set up form submission listeners
1949 $('#mxchat-url-form, #mxchat-content-form').on('submit', function() {
1950 console.log('MxChat: Form submitted, will start checking for updates');
1951 formSubmitted = true;
1952
1953 // Store submission info in sessionStorage to persist through redirects
1954 sessionStorage.setItem('mxchat_form_submitted', 'true');
1955 sessionStorage.setItem('mxchat_form_submitted_time', Date.now());
1956
1957 // Start checking for status updates right away
1958 startPolling(refreshInterval);
1959
1960 // Create a temporary message
1961 if ($('.mxchat-processing-message').length === 0) {
1962 const message = $('<div class="mxchat-processing-message" style="text-align: center; padding: 15px; background: #f0f7ff; border-radius: 8px; margin-top: 15px;">Processing request... Status will update automatically.</div>');
1963 $('.mxchat-import-section').after(message);
1964
1965 // Fade out after 5 seconds
1966 setTimeout(function() {
1967 message.fadeOut(500, function() {
1968 $(this).remove();
1969 });
1970 }, 5000);
1971 }
1972 });
1973
1974 // Listen for import option clicks
1975 $('.mxchat-import-box').on('click', function() {
1976 const option = $(this).data('option');
1977 console.log('MxChat: Import option clicked - ' + option);
1978 });
1979
1980 // Check if we recently submitted a form (within last 60 seconds for sitemap processing)
1981 if (sessionStorage.getItem('mxchat_form_submitted') === 'true') {
1982 const submittedTime = parseInt(sessionStorage.getItem('mxchat_form_submitted_time') || '0');
1983 if (Date.now() - submittedTime < 60000) { // 60 seconds
1984 console.log('MxChat: Detected recent form submission via sessionStorage');
1985 formSubmitted = true;
1986 } else {
1987 // Clear old submission data
1988 sessionStorage.removeItem('mxchat_form_submitted');
1989 sessionStorage.removeItem('mxchat_form_submitted_time');
1990 }
1991 }
1992
1993 // Attach event listener to stop button to clear the interval
1994 $('.mxchat-stop-form').on('submit', function() {
1995 console.log('MxChat: Stop processing requested, clearing update interval');
1996 stopPolling();
1997 sessionStorage.removeItem('mxchat_form_submitted');
1998 sessionStorage.removeItem('mxchat_form_submitted_time');
1999 });
2000
2001 // Start the interval for automatic updates if we have status cards or a form was submitted
2002 if (hasActiveStatus || formSubmitted) {
2003 console.log('MxChat: Starting automatic status checks');
2004 startPolling(refreshInterval);
2005 }
2006 }
2007
2008 // Function to start polling
2009 function startPolling(interval) {
2010 // Clear any existing interval first
2011 stopPolling();
2012
2013 // Do an initial fetch immediately
2014 fetchStatusUpdates();
2015
2016 // Set up new interval
2017 updateIntervalId = setInterval(function() {
2018 fetchStatusUpdates();
2019 }, interval);
2020
2021 //console.log('MxChat: Polling started with interval', interval);
2022 }
2023
2024 // Function to stop polling
2025 function stopPolling() {
2026 if (updateIntervalId !== null) {
2027 clearInterval(updateIntervalId);
2028 updateIntervalId = null;
2029 //console.log('MxChat: Polling stopped');
2030 }
2031 }
2032
2033 // Fetch status updates from the server
2034 function fetchStatusUpdates() {
2035 // If user is actively viewing the failed URLs, don't refresh as frequently
2036 const $details = $('.mxchat-failed-urls-container details');
2037 const isUserViewing = $details.length > 0 && $details.prop('open');
2038
2039 // If details are open, we'll refresh at a slower rate
2040 if (isUserViewing) {
2041 // Alternative: Update less frequently when details are open
2042 setTimeout(function() {
2043 performStatusUpdate(false); // Pass false for normal updates
2044 }, 5000); // Slow down updates to every 5 seconds when details are open
2045 } else {
2046 performStatusUpdate(false); // Pass false for normal updates
2047 }
2048 }
2049
2050 // Perform the actual AJAX request
2051 function performStatusUpdate(clearCompleted = false) {
2052 console.log('MxChat: Checking for status updates...');
2053
2054 $.ajax({
2055 url: ajaxurl,
2056 type: 'POST',
2057 data: {
2058 action: 'mxchat_get_status_updates',
2059 nonce: mxchatAdmin.status_nonce,
2060 clear_completed: clearCompleted ? 'true' : 'false'
2061 },
2062 success: function(response) {
2063 console.log('MxChat: Status update received', response);
2064
2065 // Log specific status details for debugging
2066 if (response.sitemap_status) {
2067 console.log('Sitemap status:', response.sitemap_status.status, 'Processed:', response.sitemap_status.processed_urls, 'Total:', response.sitemap_status.total_urls);
2068 }
2069
2070 // Check for completion BEFORE updating UI
2071 let shouldReload = false;
2072
2073 if (response.sitemap_status && response.sitemap_status.status === 'complete') {
2074 console.log('MxChat: Sitemap processing complete, will reload page');
2075 shouldReload = true;
2076 }
2077
2078 if (response.pdf_status && response.pdf_status.status === 'complete') {
2079 console.log('MxChat: PDF processing complete, will reload page');
2080 shouldReload = true;
2081 }
2082
2083 // Always update UI first
2084 if ((response && response.is_processing) || formSubmitted || shouldReload) {
2085 updateStatusUI(response);
2086 }
2087
2088 // Show single URL status if available and no active processing
2089 if (response.single_url_status && !response.is_processing) {
2090 updateSingleUrlStatus(response.single_url_status);
2091 }
2092
2093 // Handle completion
2094 if (shouldReload) {
2095 // Clear session storage
2096 sessionStorage.removeItem('mxchat_form_submitted');
2097 sessionStorage.removeItem('mxchat_form_submitted_time');
2098
2099 // Stop polling
2100 stopPolling();
2101
2102 // Make one more request to clear the completed status
2103 setTimeout(function() {
2104 console.log('MxChat: Clearing completed status and reloading');
2105 performStatusUpdate(true); // Pass true to clear completed
2106
2107 // Then reload after a short delay
2108 setTimeout(function() {
2109 location.reload();
2110 }, 500);
2111 }, 1500);
2112
2113 return; // Exit early
2114 }
2115
2116 // Reset form submitted flag if no active processing
2117 if (!response.is_processing) {
2118 formSubmitted = false;
2119 sessionStorage.removeItem('mxchat_form_submitted');
2120 sessionStorage.removeItem('mxchat_form_submitted_time');
2121 stopPolling();
2122 }
2123 },
2124 error: function(xhr, status, error) {
2125 console.error('MxChat: Status update failed:', error);
2126 }
2127 });
2128 }
2129
2130 // Update the UI with status information
2131 function updateStatusUI(data) {
2132 // Update PDF status if available
2133 if (data.pdf_status) {
2134 updatePdfStatus(data.pdf_status);
2135 }
2136
2137 // Update sitemap status if available
2138 if (data.sitemap_status) {
2139 updateSitemapStatus(data.sitemap_status);
2140 }
2141
2142 // Handle single URL status if available and no active processing
2143 if (data.single_url_status && !data.is_processing) {
2144 updateSingleUrlStatus(data.single_url_status);
2145 } else if (data.is_processing) {
2146 // Hide single URL status while processing
2147 $('#mxchat-single-url-status-container').hide();
2148 }
2149 }
2150
2151 // Update PDF status card
2152 function updatePdfStatus(status) {
2153 // Check if PDF card exists
2154 let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2155
2156 // If no card exists but we have status, create it
2157 if ($pdfCard.length === 0 && status) {
2158 //console.log('MxChat: Creating new PDF status card');
2159 createPdfStatusCard(status);
2160 $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2161 }
2162
2163 // If card exists, update it
2164 if ($pdfCard.length > 0) {
2165 // Update progress bar
2166 $pdfCard.find('.mxchat-progress-fill').css('width', status.percentage + '%');
2167
2168 // Update progress text
2169 $pdfCard.find('.mxchat-status-details p:first').text(
2170 'Progress: ' + status.processed_pages + ' of ' +
2171 status.total_pages + ' pages (' + status.percentage + '%)'
2172 );
2173
2174 // Update status text (if it exists)
2175 const $statusText = $pdfCard.find('.mxchat-status-details p:nth-child(2)');
2176 if ($statusText.length > 0) {
2177 $statusText.text('Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1));
2178 }
2179
2180 // Update last update text (if it exists)
2181 const $lastUpdateText = $pdfCard.find('.mxchat-status-details p:nth-child(3)');
2182 if ($lastUpdateText.length > 0) {
2183 $lastUpdateText.text('Last update: ' + status.last_update);
2184 }
2185
2186 // If we have an error, show it
2187 if (status.status === 'error' && status.error) {
2188 let $errorNotice = $pdfCard.find('.mxchat-error-notice');
2189
2190 if ($errorNotice.length === 0) {
2191 $errorNotice = $('<div class="mxchat-error-notice"><p class="error"></p></div>');
2192 $pdfCard.find('.mxchat-status-details').append($errorNotice);
2193 }
2194
2195 $errorNotice.find('p.error').text(status.error);
2196
2197 // Make sure error badge is shown
2198 if ($pdfCard.find('.mxchat-status-badge.mxchat-status-failed').length === 0) {
2199 $pdfCard.find('.mxchat-status-header').append('<span class="mxchat-status-badge mxchat-status-failed">Error</span>');
2200 }
2201 }
2202 }
2203 }
2204
2205 // Create a new PDF status card
2206 function createPdfStatusCard(status) {
2207 let html = '<div class="mxchat-status-card">';
2208 html += '<div class="mxchat-status-header">';
2209 html += '<h4>PDF Processing Status</h4>';
2210
2211 // Add stop processing form if processing
2212 if (status.status === 'processing') {
2213 html += '<form method="post" class="mxchat-stop-form" action="' +
2214 mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_stop_processing">';
2215 html += '<input type="hidden" name="mxchat_stop_processing_nonce" value="' +
2216 mxchatAdmin.stop_nonce + '">';
2217 html += '<button type="submit" name="stop_processing" class="mxchat-button-secondary">';
2218 html += 'Stop Processing</button></form>';
2219 }
2220
2221 // Add error badge if error
2222 if (status.status === 'error') {
2223 html += '<span class="mxchat-status-badge mxchat-status-failed">Error</span>';
2224 }
2225
2226 html += '</div>'; // End header
2227
2228 // Progress bar
2229 html += '<div class="mxchat-progress-bar">';
2230 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
2231 html += '</div>';
2232
2233 // Status details
2234 html += '<div class="mxchat-status-details">';
2235 html += '<p>Progress: ' + status.processed_pages + ' of ' +
2236 status.total_pages + ' pages (' + status.percentage + '%)</p>';
2237 html += '<p>Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1) + '</p>';
2238 html += '<p>Last update: ' + status.last_update + '</p>';
2239
2240 // Add error message if any
2241 if (status.status === 'error' && status.error) {
2242 html += '<div class="mxchat-error-notice">';
2243 html += '<p class="error">' + status.error + '</p>';
2244 html += '</div>';
2245 }
2246
2247 html += '</div>'; // End details
2248 html += '</div>'; // End card
2249
2250 // Try to find the import tab content to insert the status card into
2251 let $importTabContent = $('#mxchat-kb-tab-import');
2252 if ($importTabContent.length > 0) {
2253 // For the tabbed interface, add to the import tab
2254 let $sitemapCard = $importTabContent.find('.mxchat-status-card:contains("Sitemap Processing")');
2255 if ($sitemapCard.length > 0) {
2256 $sitemapCard.before($(html));
2257 } else {
2258 $importTabContent.find('.mxchat-import-section').after($(html));
2259 }
2260 } else {
2261 // Fallback to the old method
2262 let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2263 if ($sitemapCard.length > 0) {
2264 $sitemapCard.before($(html));
2265 } else {
2266 $('.mxchat-import-section').after($(html));
2267 }
2268 }
2269 }
2270
2271 // Update sitemap status card
2272 function updateSitemapStatus(status) {
2273 // Check if sitemap card exists
2274 let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2275
2276 // If no card exists but we have status, create it
2277 if ($sitemapCard.length === 0 && status) {
2278 //console.log('MxChat: Creating new sitemap status card');
2279 createSitemapStatusCard(status);
2280 $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2281 }
2282
2283 // If card exists, update it
2284 if ($sitemapCard.length > 0) {
2285 // Update progress bar
2286 $sitemapCard.find('.mxchat-progress-fill').css('width', status.percentage + '%');
2287
2288 // Update progress text
2289 $sitemapCard.find('.mxchat-status-details p:first').text(
2290 'Progress: ' + status.processed_urls + ' of ' +
2291 status.total_urls + ' URLs (' + status.percentage + '%)'
2292 );
2293
2294 // Check if details is already open before updating
2295 const isDetailsOpen = $sitemapCard.find('.mxchat-failed-urls-container details').prop('open');
2296
2297 // Update errors display
2298 let $errorContainer = $sitemapCard.find('.mxchat-error-notice');
2299
2300 if ($errorContainer.length === 0 &&
2301 (status.error || status.last_error || (status.failed_urls_list && status.failed_urls_list.length > 0))) {
2302 // Create error container if it doesn't exist
2303 $errorContainer = $('<div class="mxchat-error-notice"></div>');
2304 $sitemapCard.find('.mxchat-status-details').append($errorContainer);
2305 }
2306
2307 // Update or create error notices
2308 if ($errorContainer.length > 0) {
2309 let errorHTML = '';
2310
2311 if (status.error) {
2312 errorHTML += '<p class="error">' + status.error + '</p>';
2313 }
2314
2315 if (status.last_error) {
2316 errorHTML += '<p class="last-error">Last error: ' + status.last_error + '</p>';
2317 }
2318
2319 // Add failed URLs list
2320 if (status.failed_urls_list && status.failed_urls_list.length > 0) {
2321 errorHTML += '<div class="mxchat-failed-urls-container">';
2322 errorHTML += '<h5>Failed URLs (' + status.failed_urls_list.length + ')</h5>';
2323
2324 // Set the 'open' attribute based on previous state
2325 errorHTML += '<details' + (isDetailsOpen ? ' open' : '') + '>';
2326 errorHTML += '<summary>Show Failed URLs</summary>';
2327 errorHTML += '<div class="mxchat-failed-urls-list">';
2328
2329 // Create table for failed URLs
2330 errorHTML += '<table class="widefat striped">';
2331 errorHTML += '<thead><tr><th>URL</th><th>Error</th><th>Time</th></tr></thead>';
2332 errorHTML += '<tbody>';
2333
2334 // Sort failed URLs by most recent
2335 const sortedFailedUrls = [...status.failed_urls_list].sort((a, b) => b.time - a.time);
2336
2337 // Show up to 50 failed URLs
2338 const displayUrls = sortedFailedUrls.slice(0, 50);
2339
2340 displayUrls.forEach(item => {
2341 const timeAgo = formatTimeAgo(item.time);
2342 errorHTML += '<tr>';
2343 errorHTML += '<td style="word-break: break-all;">';
2344 errorHTML += '<a href="' + item.url + '" target="_blank" rel="noopener noreferrer">';
2345 errorHTML += truncateUrl(item.url) + '</a></td>';
2346 errorHTML += '<td>' + item.error + '</td>';
2347 errorHTML += '<td>' + timeAgo + '</td>';
2348 errorHTML += '</tr>';
2349 });
2350
2351 errorHTML += '</tbody></table>';
2352
2353 if (status.failed_urls_list.length > 50) {
2354 errorHTML += '<div class="mxchat-failed-urls-more">+ ' +
2355 (status.failed_urls_list.length - 50) +
2356 ' more failed URLs not shown</div>';
2357 }
2358
2359 errorHTML += '</div>'; // End of failed-urls-list
2360 errorHTML += '</details>';
2361 errorHTML += '</div>'; // End of failed-urls-container
2362 }
2363
2364 $errorContainer.html(errorHTML);
2365
2366 // Additionally, add a click handler to pause refreshes when viewing details
2367 $sitemapCard.find('.mxchat-failed-urls-container details').on('toggle', function() {
2368 if (this.open) {
2369 // User opened the details - set a flag
2370 $(this).data('user-opened', true);
2371 } else {
2372 // User closed the details - remove the flag
2373 $(this).data('user-opened', false);
2374 }
2375 });
2376 }
2377 }
2378 }
2379
2380 // Create a new sitemap status card
2381 function createSitemapStatusCard(status) {
2382 let html = '<div class="mxchat-status-card">';
2383 html += '<div class="mxchat-status-header">';
2384 html += '<h4>Sitemap Processing Status</h4>';
2385
2386 // Add stop processing form if processing
2387 if (status.status === 'processing') {
2388 html += '<form method="post" class="mxchat-stop-form" action="' +
2389 mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_stop_processing">';
2390 html += '<input type="hidden" name="mxchat_stop_processing_nonce" value="' +
2391 mxchatAdmin.stop_nonce + '">';
2392 html += '<button type="submit" name="stop_processing" class="mxchat-button-secondary">';
2393 html += 'Stop Processing</button></form>';
2394 }
2395
2396 // Add error badge if error
2397 if (status.status === 'error') {
2398 html += '<span class="mxchat-status-badge mxchat-status-failed">Error</span>';
2399 }
2400
2401 html += '</div>'; // End header
2402
2403 // Progress bar
2404 html += '<div class="mxchat-progress-bar">';
2405 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
2406 html += '</div>';
2407
2408 // Status details
2409 html += '<div class="mxchat-status-details">';
2410 html += '<p>Progress: ' + status.processed_urls + ' of ' +
2411 status.total_urls + ' URLs (' + status.percentage + '%)</p>';
2412
2413 // Add error message if any
2414 if ((status.error || status.last_error) && status.status === 'error') {
2415 html += '<div class="mxchat-error-notice">';
2416
2417 if (status.error) {
2418 html += '<p class="error">' + status.error + '</p>';
2419 }
2420
2421 if (status.last_error) {
2422 html += '<p class="last-error">Last error: ' + status.last_error + '</p>';
2423 }
2424
2425 html += '</div>';
2426 }
2427
2428 html += '</div>'; // End details
2429 html += '</div>'; // End card
2430
2431 // Try to find the import tab content to insert the status card into
2432 let $importTabContent = $('#mxchat-kb-tab-import');
2433 if ($importTabContent.length > 0) {
2434 // For the tabbed interface, add to the import tab
2435 let $pdfCard = $importTabContent.find('.mxchat-status-card:contains("PDF Processing")');
2436 if ($pdfCard.length > 0) {
2437 $pdfCard.after($(html));
2438 } else {
2439 $importTabContent.find('.mxchat-import-section').after($(html));
2440 }
2441 } else {
2442 // Fallback to the old method
2443 let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2444 if ($pdfCard.length > 0) {
2445 $pdfCard.after($(html));
2446 } else {
2447 $('.mxchat-import-section').after($(html));
2448 }
2449 }
2450 }
2451
2452 // Update single URL status
2453 function updateSingleUrlStatus(status) {
2454 // Check if container exists
2455 let $container = $('#mxchat-single-url-status-container');
2456
2457 if ($container.length === 0) {
2458 // Create container
2459 $container = $('<div id="mxchat-single-url-status-container"></div>');
2460
2461 // Try to find the import tab content to insert the status card into
2462 let $importTabContent = $('#mxchat-kb-tab-import');
2463 if ($importTabContent.length > 0) {
2464 // For the tabbed interface, add to the import tab
2465 let $lastStatusCard = $importTabContent.find('.mxchat-status-card').last();
2466 if ($lastStatusCard.length > 0) {
2467 $lastStatusCard.after($container);
2468 } else {
2469 $importTabContent.find('.mxchat-import-section').after($container);
2470 }
2471 } else {
2472 // Fallback to the old method
2473 let $lastStatusCard = $('.mxchat-status-card').last();
2474 if ($lastStatusCard.length > 0) {
2475 $lastStatusCard.after($container);
2476 } else {
2477 $('.mxchat-import-section').after($container);
2478 }
2479 }
2480 }
2481
2482 // Update container content
2483 let html = '<div class="mxchat-status-card">';
2484 html += '<div class="mxchat-status-header">';
2485 html += '<h4>Last URL Submission</h4>';
2486
2487 if (status.status === 'failed') {
2488 html += '<span class="mxchat-status-badge mxchat-status-failed">Failed</span>';
2489 } else {
2490 html += '<span class="mxchat-status-badge mxchat-status-success">Success</span>';
2491 }
2492
2493 html += '</div>'; // End header
2494
2495 html += '<div class="mxchat-status-details">';
2496 html += '<p><strong>URL:</strong> ';
2497 html += '<a href="' + status.url + '" target="_blank">';
2498
2499 // Truncate URL if needed
2500 const displayUrl = status.url.length > 60 ? status.url.substring(0, 57) + '...' : status.url;
2501 html += displayUrl;
2502
2503 html += '</a></p>';
2504 html += '<p><strong>Submitted:</strong> ' + status.human_time + '</p>';
2505
2506 if (status.status === 'failed' && status.error) {
2507 html += '<div class="mxchat-error-notice">';
2508 html += '<p class="error">' + status.error + '</p>';
2509 html += '</div>';
2510 }
2511
2512 if (status.status === 'complete') {
2513 html += '<p><strong>Content Length:</strong> ' + status.content_length + ' characters</p>';
2514 html += '<p><strong>Embedding Dimensions:</strong> ' + status.embedding_dimensions + '</p>';
2515 }
2516
2517 html += '</div>'; // End details
2518 html += '</div>'; // End card
2519
2520 $container.html(html).show();
2521 }
2522
2523 // Helper function to format time ago
2524 function formatTimeAgo(timestamp) {
2525 const now = Math.floor(Date.now() / 1000);
2526 const seconds = now - timestamp;
2527
2528 if (seconds < 60) {
2529 return seconds + ' seconds ago';
2530 } else if (seconds < 3600) {
2531 return Math.floor(seconds / 60) + ' minutes ago';
2532 } else if (seconds < 86400) {
2533 return Math.floor(seconds / 3600) + ' hours ago';
2534 } else {
2535 return Math.floor(seconds / 86400) + ' days ago';
2536 }
2537 }
2538
2539 // Helper function to truncate long URLs
2540 function truncateUrl(url) {
2541 const maxLength = 50;
2542 if (url.length <= maxLength) return url;
2543
2544 // Remove protocol
2545 let displayUrl = url.replace(/^https?:\/\//, '');
2546
2547 if (displayUrl.length <= maxLength) return displayUrl;
2548
2549 // Keep the domain and truncate the path
2550 const domainMatch = displayUrl.match(/^([^\/]+)\//);
2551 if (domainMatch) {
2552 const domain = domainMatch[1];
2553 const path = displayUrl.substring(domain.length);
2554
2555 if (path.length > 10) {
2556 return domain + path.substring(0, maxLength - domain.length - 3) + '...';
2557 }
2558 }
2559
2560 // Final fallback for very long strings
2561 return displayUrl.substring(0, maxLength - 3) + '...';
2562 }
2563 });
2564
2565
2566 document.addEventListener('DOMContentLoaded', function() {
2567 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2568 var modal = document.getElementById('mxchatSampleModal');
2569 var modalClose = document.getElementById('mxchatModalClose');
2570 var closeBtn = document.getElementById('mxchatCloseBtn');
2571 var copyBtn = document.getElementById('mxchatCopyBtn');
2572 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2573 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2574
2575 if (!viewSampleBtn || !modal) {
2576 return;
2577 }
2578
2579 // Open modal
2580 viewSampleBtn.addEventListener('click', function(e) {
2581 e.preventDefault();
2582 e.stopPropagation();
2583 modal.classList.add('mxchat-instructions-show');
2584 });
2585
2586 // Close modal function
2587 function closeModal(e) {
2588 if (e) {
2589 e.preventDefault();
2590 e.stopPropagation();
2591 }
2592 modal.classList.remove('mxchat-instructions-show');
2593 }
2594
2595 // Close modal events
2596 if (modalClose) {
2597 modalClose.addEventListener('click', function(e) {
2598 closeModal(e);
2599 });
2600 }
2601
2602 if (closeBtn) {
2603 closeBtn.addEventListener('click', function(e) {
2604 closeModal(e);
2605 });
2606 }
2607
2608 // Close on backdrop click ONLY (not on hover)
2609 modal.addEventListener('click', function(e) {
2610 // Only close if clicking directly on the overlay, not on child elements
2611 if (e.target === modal) {
2612 closeModal(e);
2613 }
2614 });
2615
2616 // Prevent modal content clicks from closing the modal
2617 if (modalContent) {
2618 modalContent.addEventListener('click', function(e) {
2619 e.stopPropagation();
2620 });
2621 }
2622
2623 // Close on escape key
2624 document.addEventListener('keydown', function(e) {
2625 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2626 closeModal();
2627 }
2628 });
2629
2630 // Copy functionality
2631 if (copyBtn && instructionsContent) {
2632 copyBtn.addEventListener('click', function(e) {
2633 e.preventDefault();
2634 e.stopPropagation();
2635
2636 var text = instructionsContent.textContent;
2637
2638 if (navigator.clipboard) {
2639 navigator.clipboard.writeText(text).then(function() {
2640 showCopySuccess();
2641 }).catch(function() {
2642 fallbackCopy(text);
2643 });
2644 } else {
2645 fallbackCopy(text);
2646 }
2647 });
2648 }
2649
2650 function fallbackCopy(text) {
2651 var textArea = document.createElement('textarea');
2652 textArea.value = text;
2653 textArea.style.position = 'fixed';
2654 textArea.style.left = '-999999px';
2655 textArea.style.top = '-999999px';
2656 document.body.appendChild(textArea);
2657 textArea.select();
2658 try {
2659 document.execCommand('copy');
2660 showCopySuccess();
2661 } catch (err) {
2662 console.error('Copy failed');
2663 }
2664 document.body.removeChild(textArea);
2665 }
2666
2667 function showCopySuccess() {
2668 var originalText = copyBtn.innerHTML;
2669 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!';
2670
2671 setTimeout(function() {
2672 copyBtn.innerHTML = originalText;
2673 }, 2000);
2674 }
2675 });