PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / admin / assets / js / ai-features.js

ai-features.js in wpForo Forum 3.1.6, at admin/assets/js/ai-features.js

6,920 lines 223.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * wpForo AI Features - Admin JavaScript
3 *
4 * Handles interactive elements on the AI Features admin page including:
5 * - API key reveal/hide functionality
6 * - Disconnect service confirmation dialog
7 * - Form submission with loading states
8 *
9 * @since 3.0.0
10 */
11
12 (function($) {
13 'use strict';
14
15 /**
16 * Main AI Features Admin object
17 */
18 const WpForoAI = {
19
20 // Flag to prevent multiple initializations
21 initialized: false,
22
23 /**
24 * Initialize all functionality
25 */
26 init: function() {
27 // Prevent multiple initializations
28 if (this.initialized) {
29 console.log('WpForoAI already initialized, skipping...');
30 return;
31 }
32
33 this.initialized = true;
34 this.bindEvents();
35 this.initTooltips();
36 this.initRAGFeatures();
37 this.initTagSuggest();
38 this.initCharCounters();
39 this.initBotUserSearch();
40 this.checkPostPurchaseRefresh();
41 },
42
43 /**
44 * Bind event handlers
45 */
46 bindEvents: function() {
47 // Unbind all events first to prevent duplicates
48 $(document).off('click', '.wpforo-ai-reveal-key');
49 $(document).off('click', '.wpforo-ai-disconnect-btn');
50 $(document).off('click', '.wpforo-ai-disconnect-purge-btn');
51 $(document).off('submit', '.wpforo-ai-wrap form');
52 $(document).off('click', '.wpforo-ai-copy-btn');
53 $(document).off('click', '.wpforo-ai-upgrade-btn');
54 $(document).off('click', '.wpforo-ai-buy-credits-btn');
55 $(document).off('click', '.wpforo-ai-features-accordion .accordion-header');
56 $(document).off('click', '.wpforo-ai-activate-license-btn');
57 $(document).off('click', '.wpforo-ai-activate-paddle-txn-btn');
58 $(document).off('click', '.wpforo-ai-bonus-credits-btn.eligible');
59 $(document).off('click', '.wpforo-ai-legal-link');
60 $(document).off('click', '.wpforo-ai-modal-close, .wpforo-ai-modal-close-btn, .wpforo-ai-modal-overlay');
61
62 // Reveal/hide API key
63 $(document).on('click', '.wpforo-ai-reveal-key', this.toggleApiKeyVisibility.bind(this));
64
65 // Disconnect service button
66 $(document).on('click', '.wpforo-ai-disconnect-btn', this.showDisconnectDialog.bind(this));
67
68 // Disconnect and remove all data button
69 $(document).on('click', '.wpforo-ai-disconnect-purge-btn', this.showDisconnectPurgeDialog.bind(this));
70
71 // Add loading state to form submissions (use event delegation to prevent multiple handlers)
72 $(document).on('submit', '.wpforo-ai-wrap form', this.handleFormSubmit.bind(this));
73
74 // Copy to clipboard functionality (if needed in future)
75 $(document).on('click', '.wpforo-ai-copy-btn', this.copyToClipboard.bind(this));
76
77 // Checkout - Upgrade buttons (routes to Paddle or Freemius based on selected provider)
78 $(document).on('click', '.wpforo-ai-upgrade-btn', this.handleUpgradeClick.bind(this));
79
80 // Checkout - Credit pack purchase buttons (routes to Paddle or Freemius)
81 $(document).on('click', '.wpforo-ai-buy-credits-btn', this.handleCreditPackClick.bind(this));
82
83 // Payment provider toggle
84 $(document).on('change', 'input[name="wpforo_ai_payment_provider"]', this.handleProviderChange.bind(this));
85
86 // Features accordion toggle
87 $(document).on('click', '.wpforo-ai-features-accordion .accordion-header', this.toggleAccordion.bind(this));
88
89 // License activation button
90 $(document).on('click', '.wpforo-ai-activate-license-btn', this.activateLicense.bind(this));
91
92 // Paddle transaction activation button
93 $(document).on('click', '.wpforo-ai-activate-paddle-txn-btn', this.activatePaddleTransaction.bind(this));
94
95 // Bonus credits request button
96 $(document).on('click', '.wpforo-ai-bonus-credits-btn.eligible', this.requestBonusCredits.bind(this));
97
98 // Legal document links
99 $(document).on('click', '.wpforo-ai-legal-link', this.openLegalModal.bind(this));
100
101 // Close legal modal
102 $(document).on('click', '.wpforo-ai-modal-close, .wpforo-ai-modal-close-btn, .wpforo-ai-modal-overlay', this.closeLegalModal.bind(this));
103
104 // Close modal with Escape key
105 $(document).on('keydown', this.handleModalKeydown.bind(this));
106
107 // Terms checkbox validation
108 $(document).on('submit', '#wpforo-ai-connect-form', this.validateTermsAgreement.bind(this));
109 },
110
111 /**
112 * Toggle accordion panel
113 */
114 toggleAccordion: function(e) {
115 e.preventDefault();
116
117 const $header = $(e.currentTarget);
118 const $content = $header.next('.accordion-content');
119 const isExpanded = $header.attr('aria-expanded') === 'true';
120
121 if (isExpanded) {
122 // Collapse
123 $header.attr('aria-expanded', 'false');
124 $content.slideUp(300);
125 } else {
126 // Expand
127 $header.attr('aria-expanded', 'true');
128 $content.slideDown(300);
129 }
130 },
131
132 /**
133 * Open legal document modal
134 */
135 openLegalModal: function(e) {
136 e.preventDefault();
137
138 const $link = $(e.currentTarget);
139 const documentType = $link.data('document');
140 const $modal = $('#wpforo-ai-legal-modal');
141 const $title = $('#wpforo-ai-modal-title');
142 const $content = $('#wpforo-ai-modal-content');
143
144 // Set title based on document type
145 if (documentType === 'terms') {
146 $title.text('Terms of Service');
147 } else if (documentType === 'privacy') {
148 $title.text('Privacy Policy');
149 }
150
151 // Show loading state
152 $content.html('<div class="wpforo-ai-modal-loading">Loading document...</div>');
153 $modal.show();
154 $('body').addClass('wpforo-ai-modal-open');
155
156 // Load document content via AJAX
157 $.ajax({
158 url: ajaxurl,
159 type: 'POST',
160 data: {
161 action: 'wpforo_ai_get_legal_document',
162 document: documentType,
163 nonce: wpforoAIAdmin.nonce
164 },
165 success: function(response) {
166 if (response.success && response.data.content) {
167 $content.html(response.data.content);
168 } else {
169 $content.html('<p>Error loading document. Please try again.</p>');
170 }
171 },
172 error: function() {
173 $content.html('<p>Error loading document. Please try again.</p>');
174 }
175 });
176 },
177
178 /**
179 * Close legal document modal
180 */
181 closeLegalModal: function(e) {
182 if (e) {
183 e.preventDefault();
184 }
185
186 const $modal = $('#wpforo-ai-legal-modal');
187 $modal.hide();
188 $('body').removeClass('wpforo-ai-modal-open');
189 },
190
191 /**
192 * Handle keyboard events for modal
193 */
194 handleModalKeydown: function(e) {
195 if (e.key === 'Escape' && $('#wpforo-ai-legal-modal').is(':visible')) {
196 this.closeLegalModal();
197 }
198 },
199
200 /**
201 * Validate terms agreement before form submission
202 */
203 validateTermsAgreement: function(e) {
204 const $checkbox = $('#wpforo-ai-agree-terms');
205
206 if (!$checkbox.is(':checked')) {
207 e.preventDefault();
208 alert('Please read and agree to the Terms of Service and Privacy Policy before connecting.');
209 $checkbox.focus();
210 return false;
211 }
212
213 return true;
214 },
215
216 /**
217 * Toggle API key visibility
218 */
219 toggleApiKeyVisibility: function(e) {
220 e.preventDefault();
221
222 const $button = $(e.currentTarget);
223 const $keyElement = $('.wpforo-ai-key-masked');
224 const isRevealed = $button.data('revealed') === true;
225
226 if (!isRevealed) {
227 // Show confirmation before revealing
228 if (!confirm('Are you sure you want to reveal your API key? Make sure no one is looking over your shoulder.')) {
229 return;
230 }
231
232 // Get full key from WordPress options via AJAX
233 this.fetchFullApiKey(function(fullKey) {
234 if (fullKey) {
235 $keyElement.text(fullKey);
236 $button.data('revealed', true);
237 $button.html('<span class="dashicons dashicons-hidden"></span> Hide');
238 }
239 });
240 } else {
241 // Hide the key again
242 this.fetchMaskedApiKey(function(maskedKey) {
243 $keyElement.text(maskedKey);
244 $button.data('revealed', false);
245 $button.html('<span class="dashicons dashicons-visibility"></span> Reveal');
246 });
247 }
248 },
249
250 /**
251 * Fetch full API key via AJAX
252 */
253 fetchFullApiKey: function(callback) {
254 // For now, we'll use a placeholder since AJAX endpoint isn't implemented
255 // TODO: Implement AJAX endpoint for secure key retrieval
256 const $keyElement = $('.wpforo-ai-key-masked');
257 const maskedKey = $keyElement.text();
258
259 // This is a temporary solution - in production, fetch from server
260 const mockFullKey = maskedKey.replace('***', 'XXXXXXXXXXXXXXXX');
261
262 callback(mockFullKey);
263 },
264
265 /**
266 * Fetch masked API key
267 */
268 fetchMaskedApiKey: function(callback) {
269 const $keyElement = $('.wpforo-ai-key-masked');
270 const currentText = $keyElement.text();
271 const prefix = currentText.substring(0, 6);
272 const maskedKey = prefix + '***';
273
274 callback(maskedKey);
275 },
276
277 /**
278 * Show disconnect service confirmation dialog
279 */
280 showDisconnectDialog: function(e) {
281 e.preventDefault();
282
283 const $form = $('#wpforo-ai-disconnect-form');
284
285 if (!$form.length) {
286 console.error('Disconnect form not found');
287 return;
288 }
289
290 // Use WordPress-style dialog if available
291 if (typeof wp !== 'undefined' && wp.media) {
292 // TODO: Implement custom modal with wp.media
293 this.showNativeDisconnectDialog($form);
294 } else {
295 this.showNativeDisconnectDialog($form);
296 }
297 },
298
299 /**
300 * Show native browser confirm dialog for disconnect
301 */
302 showNativeDisconnectDialog: function($form) {
303 const confirmMessage =
304 '⚠️ WARNING: This will disconnect your forum from wpForo AI service.\n\n' +
305 '⚠️ If you have an active subscription plan, please cancel it before disconnecting. Disconnecting does NOT cancel your subscription.\n\n' +
306 '• Your credits will be preserved\n' +
307 '• Your indexed content will be deleted after 30 days\n' +
308 '• You can reconnect anytime with the same site URL\n\n' +
309 'Are you absolutely sure you want to disconnect?';
310
311 if (!confirm(confirmMessage)) {
312 return;
313 }
314
315 // Ask for optional reason (user can click Cancel and still proceed)
316 const reason = prompt('Optional: Tell us why you\'re disconnecting (helps us improve):');
317
318 // Set values in form
319 $form.find('input[name="confirm"]').prop('checked', true);
320
321 if (reason && reason.trim()) {
322 $form.find('textarea[name="reason"]').val(reason.trim());
323 }
324
325 // Submit form using native DOM method (works better in Firefox after preventDefault)
326 $form[0].submit();
327 },
328
329 /**
330 * Show disconnect and remove all data confirmation dialog
331 */
332 showDisconnectPurgeDialog: function(e) {
333 e.preventDefault();
334
335 const $form = $('#wpforo-ai-disconnect-purge-form');
336
337 if (!$form.length) {
338 console.error('Disconnect purge form not found');
339 return;
340 }
341
342 const confirmMessage =
343 '⚠️ WARNING: This will PERMANENTLY DELETE ALL your data from gVectors AI servers.\n\n' +
344 '⚠️ If you have an active subscription plan, please cancel it before disconnecting. Disconnecting does NOT cancel your subscription.\n\n' +
345 '• All indexed content and embeddings will be deleted immediately\n' +
346 '• Your credits will NOT be preserved\n' +
347 '• Your tenant account will be removed\n' +
348 '• This action CANNOT be undone\n\n' +
349 'Are you absolutely sure you want to delete all data?';
350
351 if (!confirm(confirmMessage)) {
352 return;
353 }
354
355 // Double confirmation for destructive action
356 if (!confirm('This is your final confirmation. All data will be permanently deleted. Continue?')) {
357 return;
358 }
359
360 const reason = prompt('Optional: Tell us why you\'re removing your data (helps us improve):');
361
362 $form.find('input[name="confirm"]').prop('checked', true);
363
364 if (reason && reason.trim()) {
365 $form.find('textarea[name="reason"]').val(reason.trim());
366 }
367
368 $form[0].submit();
369 },
370
371 /**
372 * Handle form submission with loading state
373 */
374 handleFormSubmit: function(e) {
375 const $form = $(e.currentTarget);
376 const $submitButtons = $form.find('button[type="submit"]');
377
378 // Get the clicked button - use submitter (modern browsers) or activeElement
379 const clickedButton = e.originalEvent?.submitter || document.activeElement;
380 const $clickedButton = $(clickedButton);
381
382 // If clicked button has a name/value, update hidden field before disabling
383 // (disabled buttons don't submit their values)
384 if ($clickedButton.is('button[type="submit"]') && $clickedButton.attr('name') && $clickedButton.attr('value')) {
385 const name = $clickedButton.attr('name');
386 const value = $clickedButton.attr('value');
387 let $hidden = $form.find('input[type="hidden"][name="' + name + '"]');
388 if ($hidden.length) {
389 $hidden.val(value);
390 } else {
391 $form.prepend('<input type="hidden" name="' + name + '" value="' + value + '">');
392 }
393 }
394
395 // Add loading state to all buttons
396 $submitButtons.addClass('loading').prop('disabled', true);
397
398 // Note: Form will submit normally, this just adds visual feedback
399 // The page will reload after submission completes
400 },
401
402 /**
403 * Copy text to clipboard
404 */
405 copyToClipboard: function(e) {
406 e.preventDefault();
407
408 const $button = $(e.currentTarget);
409 const textToCopy = $button.data('copy');
410
411 if (!textToCopy) {
412 return;
413 }
414
415 // Modern clipboard API
416 if (navigator.clipboard && navigator.clipboard.writeText) {
417 navigator.clipboard.writeText(textToCopy).then(function() {
418 WpForoAI.showCopySuccess($button);
419 }).catch(function(err) {
420 console.error('Failed to copy:', err);
421 WpForoAI.fallbackCopyToClipboard(textToCopy, $button);
422 });
423 } else {
424 // Fallback for older browsers
425 this.fallbackCopyToClipboard(textToCopy, $button);
426 }
427 },
428
429 /**
430 * Fallback copy method for older browsers
431 */
432 fallbackCopyToClipboard: function(text, $button) {
433 const $temp = $('<textarea>');
434 $('body').append($temp);
435 $temp.val(text).select();
436
437 try {
438 document.execCommand('copy');
439 this.showCopySuccess($button);
440 } catch (err) {
441 console.error('Fallback copy failed:', err);
442 alert('Failed to copy to clipboard. Please copy manually.');
443 }
444
445 $temp.remove();
446 },
447
448 /**
449 * Show success feedback for copy action
450 */
451 showCopySuccess: function($button) {
452 const originalText = $button.html();
453
454 $button.html('<span class="dashicons dashicons-yes"></span> Copied!');
455 $button.addClass('copied');
456
457 setTimeout(function() {
458 $button.html(originalText);
459 $button.removeClass('copied');
460 }, 2000);
461 },
462
463 /**
464 * Open a centered popup with a loading spinner
465 */
466 openCenteredPopup: function(name, width, height) {
467 var left = (screen.width - width) / 2;
468 var top = (screen.height - height) / 2;
469 var popup = window.open('about:blank', name, 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes,resizable=yes');
470 if (popup) {
471 popup.document.write(
472 '<!DOCTYPE html><html><head><title>gVectors Store - Checkout</title>' +
473 '<style>body{margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;' +
474 'background:#f8f9fa;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}' +
475 '.loader{text-align:center;color:#555;}.spinner{width:40px;height:40px;margin:0 auto 16px;' +
476 'border:3px solid #e0e0e0;border-top:3px solid #4d9113;border-radius:50%;' +
477 'animation:spin .8s linear infinite;}@keyframes spin{to{transform:rotate(360deg)}}</style></head>' +
478 '<body><div class="loader"><div class="spinner"></div>Loading checkout...</div></body></html>'
479 );
480 popup.document.close();
481 }
482 return popup;
483 },
484
485 /**
486 * Get the currently selected payment provider
487 */
488 getSelectedProvider: function() {
489 const $checked = $('input[name="wpforo_ai_payment_provider"]:checked');
490 if ($checked.length) {
491 return $checked.val();
492 }
493 // Fallback to global default
494 return window.wpforoPaymentProvider || 'paddle';
495 },
496
497 /**
498 * Handle payment provider toggle change
499 */
500 handleProviderChange: function() {
501 window.wpforoPaymentProvider = this.getSelectedProvider();
502 },
503
504 /**
505 * Route upgrade button click to the correct provider
506 */
507 handleUpgradeClick: function(e) {
508 const provider = this.getSelectedProvider();
509 if (provider === 'paddle') {
510 this.openPaddleCheckout(e);
511 } else {
512 this.openFreemiusCheckout(e);
513 }
514 },
515
516 /**
517 * Route credit pack button click to the correct provider
518 */
519 handleCreditPackClick: function(e) {
520 const provider = this.getSelectedProvider();
521 if (provider === 'paddle') {
522 this.openPaddleCreditPackCheckout(e);
523 } else {
524 this.openCreditPackCheckout(e);
525 }
526 },
527
528 /**
529 * Open Paddle checkout in a popup window for plan upgrade
530 *
531 * Flow: AJAX to WP → backend creates checkout transaction
532 * → returns checkout URL → opens checkout page in popup
533 * → detects popup close → post-purchase refresh
534 *
535 * The checkout page is hosted on YOUR approved domain (e.g., gvectors.com),
536 * not on the customer's WordPress site. No domain verification needed per customer.
537 */
538 openPaddleCheckout: function(e) {
539 e.preventDefault();
540
541 const $button = $(e.currentTarget);
542 const plan = $button.data('plan');
543 const tenantId = $button.data('tenant-id');
544
545 // Enterprise: redirect to contact page
546 if (plan === 'enterprise') {
547 window.open('https://v3.wpforo.com/gvectors-ai/#gvai-contact', '_blank');
548 return;
549 }
550
551 // Get Paddle config
552 if (!window.wpforoPaddleCheckout || !window.wpforoPaddleCheckout.plans || !window.wpforoPaddleCheckout.plans[plan]) {
553 console.error('Paddle checkout configuration not found for plan:', plan);
554 alert('Checkout configuration error. Please try again or contact support.');
555 return;
556 }
557
558 const config = window.wpforoPaddleCheckout.plans[plan];
559 const originalText = $button.html();
560
561 // Show loading state
562 $button.prop('disabled', true).html('<span class="dashicons dashicons-update wpforo-status-spin"></span> Loading...');
563
564 // Open popup IMMEDIATELY on user click (before AJAX) to avoid popup blockers.
565 // Browsers only allow window.open() in direct click handlers — async callbacks get blocked.
566 const checkoutWindow = this.openCenteredPopup('paddle_checkout', 850, 650);
567
568 // Create checkout via AJAX → backend
569 $.ajax({
570 url: wpforoAIAdmin.ajaxUrl,
571 type: 'POST',
572 data: {
573 action: 'wpforo_ai_paddle_checkout',
574 nonce: wpforoAIAdmin.nonce,
575 price_id: config.price_id,
576 plan: plan
577 },
578 success: function(response) {
579 if (response.success && response.data.checkout_url) {
580 if (checkoutWindow && !checkoutWindow.closed) {
581 // Redirect the already-open popup to checkout URL
582 checkoutWindow.location.href = response.data.checkout_url;
583 } else {
584 // Popup was blocked or closed — fall back to redirect
585 window.location.href = response.data.checkout_url;
586 return;
587 }
588
589 // Listen for postMessage from checkout page (success signal)
590 var purchaseCompleted = false;
591 var messageHandler = function(event) {
592 if (event.data && event.data.type === 'paddle_checkout_complete') {
593 purchaseCompleted = true;
594 }
595 };
596 window.addEventListener('message', messageHandler);
597
598 // Poll for popup close — only redirect if purchase was confirmed
599 const pollTimer = setInterval(function() {
600 if (checkoutWindow.closed) {
601 clearInterval(pollTimer);
602 window.removeEventListener('message', messageHandler);
603 if (purchaseCompleted) {
604 // Redirect to post-purchase page (spinner + auto-refresh)
605 window.location.href = window.location.href.split('?')[0] +
606 '?page=wpforo-ai&upgraded=1&plan=' + encodeURIComponent(plan);
607 }
608 // If not completed, do nothing — user just closed the window
609 }
610 }, 500);
611
612 // Restore button
613 $button.prop('disabled', false).html(originalText);
614 } else {
615 // Close the blank popup on error
616 if (checkoutWindow && !checkoutWindow.closed) checkoutWindow.close();
617 const msg = (response.data && response.data.message) || 'Failed to create checkout.';
618 alert(msg + ' Please try again or contact support.');
619 $button.prop('disabled', false).html(originalText);
620 }
621 },
622 error: function() {
623 if (checkoutWindow && !checkoutWindow.closed) checkoutWindow.close();
624 alert('Failed to create checkout. Please check your connection and try again.');
625 $button.prop('disabled', false).html(originalText);
626 }
627 });
628 },
629
630 /**
631 * Open Paddle checkout in a popup window for credit pack purchase
632 */
633 openPaddleCreditPackCheckout: function(e) {
634 e.preventDefault();
635
636 const $button = $(e.currentTarget);
637 const pack = $button.data('pack');
638 const tenantId = $button.data('tenant-id');
639
640 // Get Paddle config
641 if (!window.wpforoPaddleCheckout || !window.wpforoPaddleCheckout.creditPacks || !window.wpforoPaddleCheckout.creditPacks[pack]) {
642 console.error('Paddle checkout configuration not found for credit pack:', pack);
643 alert('Checkout configuration error. Please try again or contact support.');
644 return;
645 }
646
647 const config = window.wpforoPaddleCheckout.creditPacks[pack];
648 const originalText = $button.html();
649
650 // Show loading state
651 $button.prop('disabled', true).html('<span class="dashicons dashicons-update wpforo-status-spin"></span> Loading...');
652
653 // Open popup IMMEDIATELY on user click to avoid popup blockers
654 const checkoutWindow = this.openCenteredPopup('paddle_checkout', 850, 650);
655
656 // Create checkout via AJAX → backend
657 $.ajax({
658 url: wpforoAIAdmin.ajaxUrl,
659 type: 'POST',
660 data: {
661 action: 'wpforo_ai_paddle_checkout',
662 nonce: wpforoAIAdmin.nonce,
663 price_id: config.price_id,
664 plan: 'credit_pack_' + pack
665 },
666 success: function(response) {
667 if (response.success && response.data.checkout_url) {
668 if (checkoutWindow && !checkoutWindow.closed) {
669 checkoutWindow.location.href = response.data.checkout_url;
670 } else {
671 window.location.href = response.data.checkout_url;
672 return;
673 }
674
675 // Listen for postMessage from checkout page (success signal)
676 var purchaseCompleted = false;
677 var messageHandler = function(event) {
678 if (event.data && event.data.type === 'paddle_checkout_complete') {
679 purchaseCompleted = true;
680 }
681 };
682 window.addEventListener('message', messageHandler);
683
684 // Poll for popup close — only redirect if purchase was confirmed
685 const pollTimer = setInterval(function() {
686 if (checkoutWindow.closed) {
687 clearInterval(pollTimer);
688 window.removeEventListener('message', messageHandler);
689 if (purchaseCompleted) {
690 window.location.href = window.location.href.split('?')[0] +
691 '?page=wpforo-ai&credits_purchased=1&pack=' + encodeURIComponent(pack);
692 }
693 }
694 }, 500);
695
696 $button.prop('disabled', false).html(originalText);
697 } else {
698 if (checkoutWindow && !checkoutWindow.closed) checkoutWindow.close();
699 const msg = (response.data && response.data.message) || 'Failed to create checkout.';
700 alert(msg + ' Please try again or contact support.');
701 $button.prop('disabled', false).html(originalText);
702 }
703 },
704 error: function() {
705 if (checkoutWindow && !checkoutWindow.closed) checkoutWindow.close();
706 alert('Failed to create checkout. Please check your connection and try again.');
707 $button.prop('disabled', false).html(originalText);
708 }
709 });
710 },
711
712 /**
713 * Open Freemius checkout overlay for plan upgrade
714 */
715 openFreemiusCheckout: function(e) {
716 e.preventDefault();
717
718 const $button = $(e.currentTarget);
719 const plan = $button.data('plan');
720 const tenantId = $button.data('tenant-id');
721
722 // Get checkout config from global var
723 if (!window.wpforoFreemiusCheckout || !window.wpforoFreemiusCheckout.plans || !window.wpforoFreemiusCheckout.plans[plan]) {
724 console.error('Freemius checkout configuration not found for plan:', plan);
725 if (confirm('Checkout configuration error. Please open a support ticket to quickly resolve this issue.')) {
726 window.open('https://v3.wpforo.com/login-register/?tab=login', '_blank');
727 }
728 return;
729 }
730
731 const checkoutConfig = window.wpforoFreemiusCheckout.plans[plan];
732
733 // Load Freemius Checkout JS library if not already loaded
734 if (typeof FS === 'undefined' || typeof FS.Checkout === 'undefined') {
735 this.loadFreemiusCheckoutSDK(function() {
736 WpForoAI.initFreemiusCheckout(checkoutConfig, plan, tenantId);
737 });
738 } else {
739 this.initFreemiusCheckout(checkoutConfig, plan, tenantId);
740 }
741 },
742
743 /**
744 * Load Freemius Checkout SDK dynamically
745 */
746 loadFreemiusCheckoutSDK: function(callback) {
747 // Check if already loaded
748 if (window.FS && window.FS.Checkout) {
749 callback();
750 return;
751 }
752
753 // Load the Freemius Checkout SDK
754 const script = document.createElement('script');
755 script.src = 'https://checkout.freemius.com/checkout.min.js';
756 script.async = true;
757 script.onload = callback;
758 script.onerror = function() {
759 console.error('Failed to load Freemius Checkout SDK');
760 if (confirm('Failed to load checkout. Please open a support ticket to quickly resolve this issue.')) {
761 window.open('https://v3.wpforo.com/login-register/?tab=login', '_blank');
762 }
763 };
764 document.head.appendChild(script);
765 },
766
767 /**
768 * Initialize Freemius Checkout with configuration
769 */
770 initFreemiusCheckout: function(config, plan, tenantId) {
771 console.log('Initializing Freemius checkout with config:', config);
772
773 // Validate required fields
774 if (!config.plugin_id || !config.public_key) {
775 console.error('Missing required Freemius config:', config);
776 if (confirm('Checkout configuration error. Please open a support ticket to quickly resolve this issue.')) {
777 window.open('https://v3.wpforo.com/login-register/?tab=login', '_blank');
778 }
779 return;
780 }
781
782 // Create checkout instance
783 const handler = FS.Checkout.configure({
784 plugin_id: config.plugin_id,
785 plan_id: config.plan_id,
786 pricing_id: config.pricing_id,
787 public_key: config.public_key, // Use actual public key from config
788 image: 'https://ps.w.org/wpforo/assets/icon-256x256.png'
789 });
790
791 // Build success URL with query parameters for post-purchase detection
792 const adminUrl = window.location.href.split('?')[0]; // Get base URL without query params
793 const successUrl = adminUrl + '?page=wpforo-ai&upgraded=1&plan=' + encodeURIComponent(plan);
794
795 console.log('Checkout success URL:', successUrl);
796
797 // Open the checkout overlay
798 handler.open({
799 name: 'wpForo AI Features',
800 licenses: 1,
801 billing_cycle: config.billing_cycle || 'monthly',
802 currency: config.currency || 'usd',
803 user_email: config.user ? config.user.email : '',
804 user_firstname: config.user ? config.user.first : '',
805 user_lastname: config.user ? config.user.last : '',
806 metadata: config.metadata || { tenant_id: tenantId }, // CRITICAL: Pass tenant_id in metadata
807 success_url: successUrl, // CRITICAL: Redirect URL after successful purchase
808 success: function(response) {
809 console.log('Checkout success:', response);
810 WpForoAI.handlePurchaseComplete(response, plan, tenantId);
811 },
812 cancel: function() {
813 console.log('Checkout cancelled');
814 },
815 purchaseCompleted: function(response) {
816 console.log('Purchase completed:', response);
817 WpForoAI.handlePurchaseComplete(response, plan, tenantId);
818 },
819 exitIntent: function() {
820 console.log('User exited checkout');
821 }
822 });
823 },
824
825 /**
826 * Open Freemius checkout overlay for credit pack purchase
827 */
828 openCreditPackCheckout: function(e) {
829 e.preventDefault();
830
831 const $button = $(e.currentTarget);
832 const pack = $button.data('pack');
833 const tenantId = $button.data('tenant-id');
834
835 // Get checkout config from global var
836 if (!window.wpforoFreemiusCheckout || !window.wpforoFreemiusCheckout.creditPacks || !window.wpforoFreemiusCheckout.creditPacks[pack]) {
837 console.error('Freemius checkout configuration not found for credit pack:', pack);
838 if (confirm('Checkout configuration error. Please open a support ticket to quickly resolve this issue.')) {
839 window.open('https://v3.wpforo.com/login-register/?tab=login', '_blank');
840 }
841 return;
842 }
843
844 const checkoutConfig = window.wpforoFreemiusCheckout.creditPacks[pack];
845
846 // Load Freemius Checkout JS library if not already loaded
847 if (typeof FS === 'undefined' || typeof FS.Checkout === 'undefined') {
848 this.loadFreemiusCheckoutSDK(function() {
849 WpForoAI.initCreditPackCheckout(checkoutConfig, pack, tenantId);
850 });
851 } else {
852 this.initCreditPackCheckout(checkoutConfig, pack, tenantId);
853 }
854 },
855
856 /**
857 * Initialize Freemius Checkout for credit pack purchase
858 */
859 initCreditPackCheckout: function(config, pack, tenantId) {
860 console.log('Initializing Freemius checkout for credit pack:', pack, config);
861
862 // Validate required fields
863 if (!config.plugin_id || !config.public_key) {
864 console.error('Missing required Freemius config:', config);
865 if (confirm('Checkout configuration error. Please open a support ticket to quickly resolve this issue.')) {
866 window.open('https://v3.wpforo.com/login-register/?tab=login', '_blank');
867 }
868 return;
869 }
870
871 // Create checkout instance
872 const handler = FS.Checkout.configure({
873 plugin_id: config.plugin_id,
874 plan_id: config.plan_id,
875 pricing_id: config.pricing_id,
876 public_key: config.public_key,
877 image: 'https://ps.w.org/wpforo/assets/icon-256x256.png'
878 });
879
880 // Build success URL with query parameters for post-purchase detection
881 const adminUrl = window.location.href.split('?')[0]; // Get base URL without query params
882 const successUrl = adminUrl + '?page=wpforo-ai&credits_purchased=1&pack=' + encodeURIComponent(pack);
883
884 console.log('Credit pack checkout success URL:', successUrl);
885
886 // Open the checkout overlay
887 handler.open({
888 name: 'wpForo AI Credits - ' + pack + ' Pack',
889 licenses: 1,
890 billing_cycle: 'one-time',
891 currency: config.currency || 'usd',
892 user_email: config.user ? config.user.email : '',
893 user_firstname: config.user ? config.user.first : '',
894 user_lastname: config.user ? config.user.last : '',
895 metadata: config.metadata || { tenant_id: tenantId }, // CRITICAL: Pass tenant_id in metadata
896 success_url: successUrl, // CRITICAL: Redirect URL after successful purchase
897 success: function(response) {
898 console.log('Credit pack purchase success:', response);
899 WpForoAI.handleCreditPackPurchaseComplete(response, pack, tenantId);
900 },
901 cancel: function() {
902 console.log('Credit pack checkout cancelled');
903 },
904 purchaseCompleted: function(response) {
905 console.log('Credit pack purchase completed:', response);
906 WpForoAI.handleCreditPackPurchaseComplete(response, pack, tenantId);
907 },
908 exitIntent: function() {
909 console.log('User exited credit pack checkout');
910 }
911 });
912 },
913
914 /**
915 * Handle successful purchase completion
916 */
917 handlePurchaseComplete: function(response, plan, tenantId) {
918 console.log('Purchase completed:', response);
919
920 // Show success message
921 const $notice = $('<div class="notice notice-success is-dismissible"><p><strong>Purchase Successful!</strong> Your plan has been upgraded. Refreshing page...</p></div>');
922 $('.wpforo-ai-wrap').prepend($notice);
923
924 // CRITICAL: Link subscription_id to tenant for webhook matching
925 // Freemius webhooks need this to identify the tenant
926 if (response.purchase && response.purchase.subscription_id) {
927 $.ajax({
928 url: wpforoAIAdmin.ajaxUrl,
929 type: 'POST',
930 data: {
931 action: 'wpforo_ai_link_subscription',
932 nonce: wpforoAIAdmin.nonce,
933 subscription_id: response.purchase.subscription_id,
934 user_id: response.user ? response.user.id : '',
935 plan: plan
936 },
937 success: function(linkResponse) {
938 console.log('Subscription linked:', linkResponse);
939 },
940 error: function(xhr, status, error) {
941 console.error('Failed to link subscription:', error);
942 }
943 });
944 }
945
946 // Wait a moment then reload the page to show updated plan
947 setTimeout(function() {
948 window.location.href = response.success || window.location.href.split('?')[0] + '?page=wpforo-ai&upgraded=1&plan=' + plan;
949 }, 2000);
950 },
951
952 /**
953 * Handle successful credit pack purchase completion
954 */
955 handleCreditPackPurchaseComplete: function(response, pack, tenantId) {
956 console.log('Credit pack purchase completed:', response);
957
958 // Show success message
959 const $notice = $('<div class="notice notice-success is-dismissible"><p><strong>Purchase Successful!</strong> ' + pack + ' credits have been added to your account. Refreshing page...</p></div>');
960 $('.wpforo-ai-wrap').prepend($notice);
961
962 // Wait a moment then reload the page to show updated credits
963 setTimeout(function() {
964 window.location.href = response.success || window.location.href.split('?')[0] + '?page=wpforo-ai&credits_purchased=1&pack=' + pack;
965 }, 2000);
966 },
967
968 /**
969 * Activate license manually
970 *
971 * Called when user enters a License ID and clicks Activate.
972 * Sends request to backend to verify with Freemius API.
973 */
974 activateLicense: function(e) {
975 e.preventDefault();
976
977 const $btn = $(e.currentTarget);
978 const $wrapper = $btn.closest('.license-input-wrapper');
979 const $input = $wrapper.find('#wpforo-ai-license-id');
980 const $spinner = $wrapper.find('.spinner');
981 const $result = $btn.closest('.wpforo-ai-license-activation').find('.wpforo-ai-license-result');
982 const licenseId = $input.val().trim();
983
984 // Validate input
985 if (!licenseId) {
986 $result.html('<div class="notice notice-error inline"><p>Please enter your License ID.</p></div>').show();
987 $input.focus();
988 return;
989 }
990
991 // Show loading state
992 $btn.prop('disabled', true);
993 $spinner.addClass('is-active');
994 $result.hide();
995
996 // Send AJAX request
997 $.ajax({
998 url: wpforoAIAdmin.ajaxUrl,
999 type: 'POST',
1000 data: {
1001 action: 'wpforo_ai_activate_license',
1002 nonce: wpforoAIAdmin.nonce,
1003 license_id: licenseId
1004 },
1005 success: function(response) {
1006 $btn.prop('disabled', false);
1007 $spinner.removeClass('is-active');
1008
1009 if (response.success) {
1010 const data = response.data;
1011 $result.html(
1012 '<div class="notice notice-success inline">' +
1013 '<p><strong>License Activated!</strong> ' + data.message + '</p>' +
1014 (data.plan ? '<p>Plan: <strong>' + data.plan.charAt(0).toUpperCase() + data.plan.slice(1) + '</strong></p>' : '') +
1015 (data.credits_added ? '<p>Credits added: <strong>' + data.credits_added.toLocaleString() + '</strong></p>' : '') +
1016 '</div>'
1017 ).show();
1018
1019 // Clear input
1020 $input.val('');
1021
1022 // Reload page after 2 seconds to show updated status
1023 setTimeout(function() {
1024 window.location.reload();
1025 }, 2500);
1026 } else {
1027 $result.html(
1028 '<div class="notice notice-error inline">' +
1029 '<p>' + (response.data && response.data.message ? response.data.message : 'License activation failed. Please check your License ID.') + '</p>' +
1030 '</div>'
1031 ).show();
1032 }
1033 },
1034 error: function(xhr, status, error) {
1035 $btn.prop('disabled', false);
1036 $spinner.removeClass('is-active');
1037
1038 let errorMsg = 'An error occurred. Please try again.';
1039 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
1040 errorMsg = xhr.responseJSON.data.message;
1041 }
1042
1043 $result.html(
1044 '<div class="notice notice-error inline">' +
1045 '<p>' + errorMsg + '</p>' +
1046 '</div>'
1047 ).show();
1048 }
1049 });
1050 },
1051
1052 /**
1053 * Activate Paddle transaction manually (mirrors activateLicense)
1054 */
1055 activatePaddleTransaction: function(e) {
1056 e.preventDefault();
1057
1058 const $btn = $(e.currentTarget);
1059 const $wrapper = $btn.closest('.license-input-wrapper');
1060 const $input = $wrapper.find('#wpforo-ai-paddle-txn-id');
1061 const $spinner = $wrapper.find('.spinner');
1062 const $result = $btn.closest('.wpforo-ai-paddle-activation').find('.wpforo-ai-paddle-result');
1063 const txnId = $input.val().trim();
1064
1065 // Validate input
1066 if (!txnId) {
1067 $result.html('<div class="notice notice-error inline"><p>Please enter your Transaction ID.</p></div>').show();
1068 $input.focus();
1069 return;
1070 }
1071
1072 if (txnId.indexOf('txn_') !== 0) {
1073 $result.html('<div class="notice notice-error inline"><p>Invalid Transaction ID format. Must start with "txn_".</p></div>').show();
1074 $input.focus();
1075 return;
1076 }
1077
1078 // Show loading state
1079 $btn.prop('disabled', true);
1080 $spinner.addClass('is-active');
1081 $result.hide();
1082
1083 // Send AJAX request
1084 $.ajax({
1085 url: wpforoAIAdmin.ajaxUrl,
1086 type: 'POST',
1087 data: {
1088 action: 'wpforo_ai_activate_paddle_transaction',
1089 nonce: wpforoAIAdmin.nonce,
1090 transaction_id: txnId
1091 },
1092 success: function(response) {
1093 $btn.prop('disabled', false);
1094 $spinner.removeClass('is-active');
1095
1096 if (response.success) {
1097 const data = response.data;
1098 $result.html(
1099 '<div class="notice notice-success inline">' +
1100 '<p><strong>Transaction Activated!</strong> ' + data.message + '</p>' +
1101 (data.plan ? '<p>Plan: <strong>' + data.plan.charAt(0).toUpperCase() + data.plan.slice(1) + '</strong></p>' : '') +
1102 (data.credits_added ? '<p>Credits added: <strong>' + data.credits_added.toLocaleString() + '</strong></p>' : '') +
1103 '</div>'
1104 ).show();
1105
1106 // Clear input
1107 $input.val('');
1108
1109 // Reload page after 2 seconds to show updated status
1110 setTimeout(function() {
1111 window.location.reload();
1112 }, 2500);
1113 } else {
1114 $result.html(
1115 '<div class="notice notice-error inline">' +
1116 '<p>' + (response.data && response.data.message ? response.data.message : 'Transaction activation failed. Please check your Transaction ID.') + '</p>' +
1117 '</div>'
1118 ).show();
1119 }
1120 },
1121 error: function(xhr, status, error) {
1122 $btn.prop('disabled', false);
1123 $spinner.removeClass('is-active');
1124
1125 let errorMsg = 'An error occurred. Please try again.';
1126 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
1127 errorMsg = xhr.responseJSON.data.message;
1128 }
1129
1130 $result.html(
1131 '<div class="notice notice-error inline">' +
1132 '<p>' + errorMsg + '</p>' +
1133 '</div>'
1134 ).show();
1135 }
1136 });
1137 },
1138
1139 /**
1140 * Request bonus credits for large forums
1141 */
1142 requestBonusCredits: function(e) {
1143 e.preventDefault();
1144 e.stopPropagation();
1145 e.stopImmediatePropagation();
1146
1147 const $btn = $(e.currentTarget);
1148
1149 // Prevent double-click
1150 if ($btn.hasClass('loading') || $btn.prop('disabled')) {
1151 return;
1152 }
1153 const $spinner = $btn.siblings('.wpforo-ai-bonus-spinner');
1154
1155 // Confirm dialog
1156 const confirmMessage =
1157 '🎁 Request Free Indexing Credits\n\n' +
1158 'This is a one-time bonus for large forums.\n' +
1159 'Credits will be added based on your topic count.\n\n' +
1160 'Do you want to proceed?';
1161
1162 if (!confirm(confirmMessage)) {
1163 return;
1164 }
1165
1166 // Show loading state - spin the icon
1167 $btn.addClass('loading').prop('disabled', true);
1168 $btn.find('.dashicons').addClass('dashicons-update dashicons-spin').removeClass('dashicons-star-filled');
1169 $spinner.addClass('is-active');
1170
1171 // Timer to show progress - 60 second timeout
1172 let seconds = 0;
1173 const originalText = $btn.html();
1174 const timerInterval = setInterval(function() {
1175 seconds++;
1176 // Update button text to show countdown to refresh
1177 $btn.contents().filter(function() {
1178 return this.nodeType === 3; // Text nodes only
1179 }).remove();
1180 $btn.append(' Processing... (' + seconds + 's)');
1181 }, 1000);
1182
1183 // Send AJAX request with extended timeout
1184 $.ajax({
1185 url: wpforoAIAdmin.ajaxUrl,
1186 type: 'POST',
1187 timeout: 60000, // 60 second timeout
1188 data: {
1189 action: 'wpforo_ai_request_bonus_credits',
1190 _wpnonce: wpforoAIAdmin.nonce
1191 },
1192 success: function(response) {
1193 clearInterval(timerInterval);
1194 $spinner.removeClass('is-active');
1195
1196 if (response.success) {
1197 const data = response.data;
1198 const creditsAdded = data.credits_added || 0;
1199
1200 // Show success message
1201 alert('�
1202 Success!\n\n' + data.message + '\n\nCredits added: ' + creditsAdded.toLocaleString());
1203
1204 // Update button to show claimed state
1205 $btn.removeClass('eligible loading')
1206 .addClass('claimed')
1207 .prop('disabled', true)
1208 .html('<span class="dashicons dashicons-awards"></span> Extra Free Credits ' + creditsAdded.toLocaleString());
1209
1210 // Reload page after 2 seconds to update credit display
1211 setTimeout(function() {
1212 window.location.reload();
1213 }, 2000);
1214 } else {
1215 // Re-enable button on error
1216 $btn.removeClass('loading').prop('disabled', false).html(originalText);
1217
1218 const errorMsg = response.data && response.data.message
1219 ? response.data.message
1220 : 'Failed to request bonus credits.';
1221 alert('❌ Error\n\n' + errorMsg);
1222 }
1223 },
1224 error: function(xhr, status, error) {
1225 clearInterval(timerInterval);
1226 $btn.removeClass('loading').prop('disabled', false).html(originalText);
1227 $spinner.removeClass('is-active');
1228
1229 let errorMsg = 'An error occurred. Please try again.';
1230 if (status === 'timeout') {
1231 errorMsg = 'Request timed out. Please try again.';
1232 } else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
1233 errorMsg = xhr.responseJSON.data.message;
1234 }
1235
1236 alert('❌ Error\n\n' + errorMsg);
1237 }
1238 });
1239 },
1240
1241 /**
1242 * Initialize tooltips (if needed)
1243 */
1244 initTooltips: function() {
1245 // Add WordPress-style tooltips to elements with title attributes
1246 $('[data-tooltip]').each(function() {
1247 const $el = $(this);
1248 const tooltipText = $el.data('tooltip');
1249
1250 if (tooltipText) {
1251 $el.attr('title', tooltipText);
1252 }
1253 });
1254 },
1255
1256 /**
1257 * Show notification message
1258 */
1259 showNotice: function(message, type) {
1260 type = type || 'info'; // info, success, warning, error
1261
1262 const $notice = $('<div>')
1263 .addClass('notice notice-' + type + ' is-dismissible')
1264 .append($('<p>').text(message));
1265
1266 // Insert notice after page title
1267 $('.wpforo-ai-title').after($notice);
1268
1269 // Auto-dismiss after 5 seconds
1270 setTimeout(function() {
1271 $notice.fadeOut(function() {
1272 $(this).remove();
1273 });
1274 }, 5000);
1275
1276 // Make dismissible
1277 $(document).trigger('wp-updates-notice-added');
1278 },
1279
1280 /**
1281 * Format numbers with thousand separators
1282 */
1283 formatNumber: function(num) {
1284 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1285 },
1286
1287 /**
1288 * Validate form before submission
1289 */
1290 validateForm: function($form) {
1291 let isValid = true;
1292 const requiredFields = $form.find('[required]');
1293
1294 requiredFields.each(function() {
1295 const $field = $(this);
1296 const value = $field.val().trim();
1297
1298 if (!value) {
1299 isValid = false;
1300 $field.addClass('error');
1301 $field.on('input change', function() {
1302 $(this).removeClass('error');
1303 });
1304 }
1305 });
1306
1307 if (!isValid) {
1308 alert('Please fill in all required fields.');
1309 }
1310
1311 return isValid;
1312 },
1313
1314 /**
1315 * Initialize RAG-specific features
1316 */
1317 initRAGFeatures: function() {
1318 // Check if indexing was being stopped before page reload
1319 if (localStorage.getItem('wpforo_indexing_stopping') === 'true') {
1320 this.indexingStopping = true;
1321 // Update status to show "Stopping..." if still processing
1322 const $statusElement = $('#rag-indexing-status');
1323 const statusText = $statusElement.text().trim();
1324 // Only show "Stopping..." if status indicates processing (not idle)
1325 if ($statusElement.length && statusText !== 'Idle') {
1326 $statusElement.text('Stopping...');
1327 } else if (statusText === 'Idle') {
1328 // Process already stopped, clear the flag
1329 this.indexingStopping = false;
1330 localStorage.removeItem('wpforo_indexing_stopping');
1331 }
1332 }
1333
1334 // Unbind first to prevent duplicate handlers
1335 $(document).off('click', '.wpforo-ai-reindex-all');
1336 $(document).off('click', '.wpforo-ai-reindex-images');
1337 $(document).off('click', '.wpforo-ai-clear-database');
1338 $(document).off('click', '.wpforo-ai-clear-and-reindex');
1339 $(document).off('click', '.wpforo-ai-stop-indexing');
1340 $(document).off('click', '.wpforo-ai-cleanup-session');
1341 $(document).off('submit', '#wpforo-ai-search-test-form');
1342
1343 // Bind bulk action buttons
1344 $(document).on('click', '.wpforo-ai-reindex-all', this.handleReindexAll.bind(this));
1345 $(document).on('click', '.wpforo-ai-reindex-images', this.handleReindexImages.bind(this));
1346 $(document).on('click', '.wpforo-ai-clear-database', this.handleClearDatabase.bind(this));
1347 $(document).on('click', '.wpforo-ai-clear-and-reindex', this.handleClearAndReindex.bind(this));
1348 $(document).on('click', '.wpforo-ai-stop-indexing', this.handleStopIndexing.bind(this));
1349 $(document).on('click', '.wpforo-ai-cleanup-session', this.handleCleanupSession.bind(this));
1350
1351 // Bind search test form
1352 $(document).on('submit', '#wpforo-ai-search-test-form', this.handleSearchTest.bind(this));
1353
1354 // Bind storage mode toggle
1355 $(document).off('change', 'input[name="wpforo_ai_storage_mode"]');
1356 $(document).on('change', 'input[name="wpforo_ai_storage_mode"]', this.handleStorageModeChange.bind(this));
1357
1358 // Bind auto-indexing toggle
1359 $(document).off('change', '#wpforo-ai-auto-indexing');
1360 $(document).on('change', '#wpforo-ai-auto-indexing', this.handleAutoIndexingToggle.bind(this));
1361
1362 // Bind image indexing toggle
1363 $(document).off('change', '#wpforo-ai-image-indexing');
1364 $(document).on('change', '#wpforo-ai-image-indexing', this.handleImageIndexingToggle.bind(this));
1365
1366 // Bind document indexing toggle
1367 $(document).off('change', '#wpforo-ai-document-indexing');
1368 $(document).on('change', '#wpforo-ai-document-indexing', this.handleDocumentIndexingToggle.bind(this));
1369
1370 // Bind refresh status button
1371 $(document).off('click', '.wpforo-ai-refresh-rag-status');
1372 $(document).on('click', '.wpforo-ai-refresh-rag-status', this.handleRefreshStatus.bind(this));
1373
1374 // Check for in-progress local indexing and auto-resume
1375 this.checkLocalIndexingProgress();
1376
1377 // Check for in-progress cloud indexing auto-refresh (survives page reloads)
1378 this.checkForumIndexingAutoRefresh();
1379
1380 // Load indexing breakdown asynchronously (cached 1 day)
1381 this.loadIndexingBreakdown();
1382
1383 // Note: Polling is started from PHP inline script based on server-side $is_indexing status
1384 // No need to start it here to avoid duplicate polling
1385 },
1386
1387 /**
1388 * Handle storage mode toggle change
1389 */
1390 handleStorageModeChange: function(e) {
1391 const $input = $(e.currentTarget);
1392 const newMode = $input.val();
1393 const $container = $input.closest('.wpforo-ai-storage-toggle');
1394
1395 // Update active state on labels
1396 $container.find('.wpforo-ai-storage-option').removeClass('active');
1397 $input.next('label').addClass('active');
1398
1399 // Get the current board ID from URL
1400 const urlParams = new URLSearchParams(window.location.search);
1401 const boardId = urlParams.get('boardid') || 0;
1402
1403 // Save via AJAX
1404 $.ajax({
1405 url: wpforoAIAdmin.ajaxUrl,
1406 type: 'POST',
1407 data: {
1408 action: 'wpforo_ai_save_storage_mode',
1409 nonce: wpforoAIAdmin.nonce,
1410 storage_mode: newMode,
1411 board_id: boardId
1412 },
1413 beforeSend: function() {
1414 $container.css('opacity', '0.6');
1415 },
1416 success: function(response) {
1417 $container.css('opacity', '1');
1418 if (response.success) {
1419 // Reload page to update storage info section
1420 window.location.reload();
1421 } else {
1422 alert(response.data?.message || 'Failed to save storage mode.');
1423 // Revert the change
1424 window.location.reload();
1425 }
1426 },
1427 error: function() {
1428 $container.css('opacity', '1');
1429 alert('Error saving storage mode. Please try again.');
1430 window.location.reload();
1431 }
1432 });
1433 },
1434
1435 /**
1436 * Handle auto-indexing toggle change
1437 */
1438 handleAutoIndexingToggle: function(e) {
1439 const $input = $(e.currentTarget);
1440 const isEnabled = $input.is(':checked') ? 1 : 0;
1441 const boardId = $input.data('board-id') || 0;
1442 const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
1443
1444 // Disable the toggle during AJAX request
1445 $input.prop('disabled', true);
1446 $toggle.css('opacity', '0.6');
1447
1448 // Save via AJAX
1449 $.ajax({
1450 url: wpforoAIAdmin.ajaxUrl,
1451 type: 'POST',
1452 data: {
1453 action: 'wpforo_ai_save_auto_indexing',
1454 nonce: wpforoAIAdmin.nonce,
1455 enabled: isEnabled,
1456 board_id: boardId
1457 },
1458 success: function(response) {
1459 $input.prop('disabled', false);
1460 $toggle.css('opacity', '1');
1461 if (!response.success) {
1462 // Revert the change on failure
1463 $input.prop('checked', !isEnabled);
1464 alert(response.data?.message || 'Failed to save auto-indexing setting.');
1465 }
1466 },
1467 error: function() {
1468 $input.prop('disabled', false);
1469 $toggle.css('opacity', '1');
1470 // Revert the change on error
1471 $input.prop('checked', !isEnabled);
1472 alert('Error saving auto-indexing setting. Please try again.');
1473 }
1474 });
1475 },
1476
1477 /**
1478 * Handle image indexing toggle change
1479 *
1480 * When enabled, posts with images will consume +1 additional credit
1481 * for multimodal processing (image → text → embedding).
1482 * Requires Business or Enterprise plan.
1483 */
1484 handleImageIndexingToggle: function(e) {
1485 const $input = $(e.currentTarget);
1486 const isEnabled = $input.is(':checked') ? 1 : 0;
1487 const boardId = $input.data('board-id') || 0;
1488 const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
1489
1490 // Show confirmation when enabling (due to credit impact)
1491 if (isEnabled) {
1492 const confirmed = confirm(
1493 'Enable Image Indexing?\n\n' +
1494 'When enabled, posts with images will consume +1 additional credit during indexing.\n\n' +
1495 '• Maximum 10 images per post are processed\n' +
1496 '• Images are converted to text descriptions for search\n' +
1497 '• Small images (< 50x50px) like smileys are skipped\n\n' +
1498 'Continue?'
1499 );
1500 if (!confirmed) {
1501 $input.prop('checked', false);
1502 return;
1503 }
1504 }
1505
1506 // Disable the toggle during AJAX request
1507 $input.prop('disabled', true);
1508 $toggle.css('opacity', '0.6');
1509
1510 // Save via AJAX
1511 $.ajax({
1512 url: wpforoAIAdmin.ajaxUrl,
1513 type: 'POST',
1514 data: {
1515 action: 'wpforo_ai_save_image_indexing',
1516 nonce: wpforoAIAdmin.nonce,
1517 enabled: isEnabled,
1518 board_id: boardId
1519 },
1520 success: function(response) {
1521 $input.prop('disabled', false);
1522 $toggle.css('opacity', '1');
1523 if (response.success) {
1524 // Show success message
1525 if (response.data?.message) {
1526 // Brief notification instead of alert
1527 console.log('Image indexing: ' + response.data.message);
1528 }
1529 } else {
1530 // Revert the change on failure
1531 $input.prop('checked', !isEnabled);
1532 alert(response.data?.message || 'Failed to save image indexing setting.');
1533 }
1534 },
1535 error: function() {
1536 $input.prop('disabled', false);
1537 $toggle.css('opacity', '1');
1538 // Revert the change on error
1539 $input.prop('checked', !isEnabled);
1540 alert('Error saving image indexing setting. Please try again.');
1541 }
1542 });
1543 },
1544
1545 /**
1546 * Handle document indexing toggle change
1547 */
1548 handleDocumentIndexingToggle: function(e) {
1549 const $input = $(e.currentTarget);
1550 const isEnabled = $input.is(':checked') ? 1 : 0;
1551 const boardId = $input.data('board-id') || 0;
1552 const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
1553
1554 // Show confirmation when enabling (due to credit impact)
1555 if (isEnabled) {
1556 const confirmed = confirm(
1557 'Enable Document Indexing?\n\n' +
1558 'When enabled, document attachments (PDF, DOCX, PPTX, etc.) will be processed during indexing.\n\n' +
1559 '• Maximum 5 documents per post\n' +
1560 '• Text is extracted from documents for search\n' +
1561 '• Credit cost: 1 per page\n\n' +
1562 'Continue?'
1563 );
1564 if (!confirmed) {
1565 $input.prop('checked', false);
1566 return;
1567 }
1568 }
1569
1570 // Disable the toggle during AJAX request
1571 $input.prop('disabled', true);
1572 $toggle.css('opacity', '0.6');
1573
1574 // Save via AJAX
1575 $.ajax({
1576 url: wpforoAIAdmin.ajaxUrl,
1577 type: 'POST',
1578 data: {
1579 action: 'wpforo_ai_save_document_indexing',
1580 nonce: wpforoAIAdmin.nonce,
1581 enabled: isEnabled,
1582 board_id: boardId
1583 },
1584 success: function(response) {
1585 $input.prop('disabled', false);
1586 $toggle.css('opacity', '1');
1587 if (response.success) {
1588 if (response.data?.message) {
1589 console.log('Document indexing: ' + response.data.message);
1590 }
1591 } else {
1592 $input.prop('checked', !isEnabled);
1593 alert(response.data?.message || 'Failed to save document indexing setting.');
1594 }
1595 },
1596 error: function() {
1597 $input.prop('disabled', false);
1598 $toggle.css('opacity', '1');
1599 $input.prop('checked', !isEnabled);
1600 alert('Error saving document indexing setting. Please try again.');
1601 }
1602 });
1603 },
1604
1605 /**
1606 * Handle refresh status button click
1607 */
1608 handleRefreshStatus: function(e) {
1609 e.preventDefault();
1610 const $button = $(e.currentTarget);
1611 const $icon = $button.find('.dashicons-update');
1612
1613 // Add spinning animation
1614 $icon.addClass('wpforo-spin');
1615 $button.prop('disabled', true);
1616
1617 // Store reference for callback
1618 const self = this;
1619
1620 // Refresh status via AJAX
1621 $.ajax({
1622 url: wpforoAIAdmin.ajaxUrl,
1623 type: 'POST',
1624 data: {
1625 action: 'wpforo_ai_get_rag_status',
1626 nonce: wpforoAIAdmin.nonce
1627 },
1628 success: function(response) {
1629 if (response.success && response.data) {
1630 self.updateRAGStatusDisplay(response.data);
1631 }
1632 },
1633 error: function(xhr, status, error) {
1634 console.error('Failed to refresh RAG status:', error);
1635 },
1636 complete: function() {
1637 // Stop spinning animation
1638 $icon.removeClass('wpforo-spin');
1639 $button.prop('disabled', false);
1640 }
1641 });
1642 },
1643
1644 /**
1645 * Load indexing breakdown via AJAX (private/unapproved topic counts)
1646 * Data is cached server-side for 1 day to avoid slow GROUP BY queries
1647 */
1648 loadIndexingBreakdown: function() {
1649 const $container = $('#wpforo-ai-indexing-breakdown-container');
1650 if (!$container.length) {
1651 return;
1652 }
1653
1654 const self = this;
1655 const loadingText = $container.data('loading-text') || 'Loading...';
1656
1657 // Show small loading spinner
1658 $container.html('<span class="wpforo-ai-breakdown-loading"><span class="dashicons dashicons-update wpforo-spin"></span> ' + loadingText + '</span>');
1659
1660 $.ajax({
1661 url: wpforoAIAdmin.ajaxUrl,
1662 type: 'POST',
1663 data: {
1664 action: 'wpforo_ai_get_indexing_breakdown',
1665 nonce: wpforoAIAdmin.nonce
1666 },
1667 success: function(response) {
1668 if (response.success && response.data) {
1669 self.renderIndexingBreakdown($container, response.data);
1670 } else {
1671 $container.empty();
1672 }
1673 },
1674 error: function() {
1675 $container.empty();
1676 }
1677 });
1678 },
1679
1680 /**
1681 * Render the indexing breakdown HTML
1682 */
1683 renderIndexingBreakdown: function($container, data) {
1684 const privateCount = parseInt(data.private, 10) || 0;
1685 const unapprovedCount = parseInt(data.unapproved, 10) || 0;
1686
1687 if (privateCount === 0 && unapprovedCount === 0) {
1688 $container.empty();
1689 return;
1690 }
1691
1692 const excludedCount = privateCount + unapprovedCount;
1693 const excludedText = $container.data('excluded-text') || '%s topics are excluded from indexing';
1694 const introText = $container.data('intro-text') || 'The following topics are automatically excluded from AI indexing:';
1695 const privateText = $container.data('private-text') || 'private topics - these are only visible to their authors';
1696 const unapprovedText = $container.data('unapproved-text') || 'unapproved topics - these will be indexed once approved by moderators';
1697 const noteText = $container.data('note-text') || 'Private topics are never indexed to protect user privacy. Unapproved topics will be automatically indexed when approved.';
1698
1699 let html = '<div class="wpforo-ai-indexing-breakdown">';
1700 html += '<details class="wpforo-ai-breakdown-details">';
1701 html += '<summary class="wpforo-ai-breakdown-summary">';
1702 html += '<span class="dashicons dashicons-info-outline"></span>';
1703 html += excludedText.replace('%s', '<strong>' + this.formatNumber(excludedCount) + '</strong>');
1704 html += '<span class="dashicons dashicons-arrow-down-alt2 wpforo-ai-breakdown-arrow"></span>';
1705 html += '</summary>';
1706 html += '<div class="wpforo-ai-breakdown-content">';
1707 html += '<p class="wpforo-ai-breakdown-intro">' + introText + '</p>';
1708 html += '<ul class="wpforo-ai-breakdown-list">';
1709
1710 if (privateCount > 0) {
1711 html += '<li><span class="dashicons dashicons-lock"></span>';
1712 html += '<strong>' + this.formatNumber(privateCount) + '</strong> ' + privateText + '</li>';
1713 }
1714
1715 if (unapprovedCount > 0) {
1716 html += '<li><span class="dashicons dashicons-clock"></span>';
1717 html += '<strong>' + this.formatNumber(unapprovedCount) + '</strong> ' + unapprovedText + '</li>';
1718 }
1719
1720 html += '</ul>';
1721 html += '<p class="wpforo-ai-breakdown-note"><em>' + noteText + '</em></p>';
1722 html += '</div></details></div>';
1723
1724 $container.html(html);
1725 },
1726
1727 // WordPress Content Indexing Methods have been moved to
1728 // ai-features-wp-indexing.js for the dedicated WordPress Indexing tab.
1729 // See: WpForoWPIndexing in admin/assets/js/ai-features-wp-indexing.js
1730
1731 /**
1732 * Initialize tag autocomplete using WordPress suggest script
1733 */
1734 initTagSuggest: function() {
1735 var $tagInput = $('.wpforo-ai-tags-input');
1736 if ($tagInput.length && typeof $.fn.suggest === 'function' && typeof wpforoAIAdmin !== 'undefined') {
1737 var ajaxUrl = wpforoAIAdmin.ajaxUrl;
1738 $tagInput.suggest(
1739 ajaxUrl + (ajaxUrl.indexOf('?') !== -1 ? '&' : '?') + 'action=wpforo_tag_search',
1740 {
1741 multiple: true,
1742 multipleSep: ',',
1743 delay: 500,
1744 minchars: 2,
1745 resultsClass: 'wpforo-ai-tag-results',
1746 selectClass: 'wpforo-ai-tag-over',
1747 matchClass: 'wpforo-ai-tag-match'
1748 }
1749 );
1750 }
1751 },
1752
1753 /**
1754 * Initialize Bot User Search autocomplete for AI Bot Reply settings
1755 */
1756 initBotUserSearch: function() {
1757 const self = this;
1758 const $searchInput = $('#wpforo-ai-bot-user-search');
1759
1760 // Only init if the search input exists (settings page with Bot Reply section)
1761 if (!$searchInput.length) {
1762 return;
1763 }
1764
1765 const $wrapper = $searchInput.closest('.wpforo-ai-user-search-wrapper');
1766 const $hiddenInput = $wrapper.find('.wpforo-ai-user-id-input');
1767 const $resultsContainer = $wrapper.find('.wpforo-ai-user-search-results');
1768 const nonce = $('#wpforo_ai_bot_user_nonce').val() || '';
1769 let searchTimeout = null;
1770
1771 // Handle input for search
1772 $searchInput.on('input', function() {
1773 const searchTerm = $(this).val().trim();
1774
1775 // Clear previous timeout
1776 if (searchTimeout) {
1777 clearTimeout(searchTimeout);
1778 }
1779
1780 // Clear results if search term is too short
1781 if (searchTerm.length < 2) {
1782 $resultsContainer.empty().hide();
1783 return;
1784 }
1785
1786 // Debounce the search
1787 searchTimeout = setTimeout(function() {
1788 self.searchBotUsers(searchTerm, $resultsContainer, $hiddenInput, $searchInput, nonce);
1789 }, 300);
1790 });
1791
1792 // Handle click outside to close results
1793 $(document).on('click', function(e) {
1794 if (!$(e.target).closest('.wpforo-ai-user-search-wrapper').length) {
1795 $resultsContainer.empty().hide();
1796 }
1797 });
1798
1799 // Handle focus to show results if there's a search term
1800 $searchInput.on('focus', function() {
1801 if ($(this).val().trim().length >= 2 && $resultsContainer.children().length > 0) {
1802 $resultsContainer.show();
1803 }
1804 });
1805 },
1806
1807 /**
1808 * Perform AJAX search for bot users
1809 */
1810 searchBotUsers: function(searchTerm, $resultsContainer, $hiddenInput, $searchInput, nonce) {
1811 $resultsContainer.html('<div class="wpforo-ai-user-search-loading">Searching...</div>').show();
1812
1813 $.ajax({
1814 url: ajaxurl,
1815 type: 'POST',
1816 data: {
1817 action: 'wpforo_ai_search_bot_users',
1818 search: searchTerm,
1819 _wpnonce: nonce
1820 },
1821 success: function(response) {
1822 $resultsContainer.empty();
1823
1824 if (response.success && response.data.users && response.data.users.length > 0) {
1825 const $list = $('<ul class="wpforo-ai-user-search-list"></ul>');
1826
1827 response.data.users.forEach(function(user) {
1828 const $item = $('<li class="wpforo-ai-user-search-item" data-user-id="' + user.id + '"></li>');
1829 $item.text(user.label);
1830 $item.on('click', function() {
1831 $hiddenInput.val(user.id);
1832 $searchInput.val(user.label);
1833 $resultsContainer.empty().hide();
1834 // Clear usergroup when specific user is selected
1835 $hiddenInput.closest('.wpforo-ai-form-section').find('.wpforo-ai-author-groupid-select').val('');
1836 });
1837 $list.append($item);
1838 });
1839
1840 $resultsContainer.append($list).show();
1841 } else {
1842 $resultsContainer.html('<div class="wpforo-ai-user-search-empty">No users found</div>').show();
1843 }
1844 },
1845 error: function() {
1846 $resultsContainer.html('<div class="wpforo-ai-user-search-error">Search error</div>').show();
1847 }
1848 });
1849 },
1850
1851 /**
1852 * Initialize character counters for textareas with limits
1853 * Uses proper character counting that works with multibyte characters
1854 */
1855 initCharCounters: function() {
1856 const self = this;
1857
1858 // Find all textareas with data-char-limit attribute
1859 $(document).on('input', 'textarea[data-char-limit]', function() {
1860 self.updateCharCounter($(this));
1861 });
1862
1863 // Also handle when form fields are populated (e.g., when editing a task)
1864 $(document).on('wpforo-ai-task-loaded', function() {
1865 $('textarea[data-char-limit]').each(function() {
1866 self.updateCharCounter($(this));
1867 });
1868 });
1869
1870 // Initialize counters on page load
1871 $('textarea[data-char-limit]').each(function() {
1872 self.updateCharCounter($(this));
1873 });
1874 },
1875
1876 /**
1877 * Update character counter for a textarea
1878 * Uses string spread operator for proper Unicode character counting
1879 */
1880 updateCharCounter: function($textarea) {
1881 const limit = parseInt($textarea.data('char-limit'), 10) || 120;
1882 const $counter = $textarea.siblings('.wpforo-ai-char-counter').find('.current');
1883 const $counterWrapper = $textarea.siblings('.wpforo-ai-char-counter');
1884
1885 if (!$counter.length) {
1886 return;
1887 }
1888
1889 // Use spread operator to properly count Unicode characters (multibyte safe)
1890 const text = $textarea.val() || '';
1891 const charCount = [...text].length;
1892
1893 $counter.text(charCount);
1894
1895 // Update counter styling based on proximity to limit
1896 $counterWrapper.removeClass('warning limit');
1897 if (charCount >= limit) {
1898 $counterWrapper.addClass('limit');
1899 } else if (charCount >= limit * 0.8) {
1900 $counterWrapper.addClass('warning');
1901 }
1902
1903 // Enforce limit (multibyte safe truncation)
1904 if (charCount > limit) {
1905 const truncated = [...text].slice(0, limit).join('');
1906 $textarea.val(truncated);
1907 $counter.text(limit);
1908 $counterWrapper.addClass('limit');
1909 }
1910 },
1911
1912 /**
1913 * Scroll to the Indexing Status section
1914 */
1915 scrollToIndexingStatus: function() {
1916 const $statusBox = $('.wpforo-ai-rag-status-box');
1917 if ($statusBox.length) {
1918 $('html, body').animate({
1919 scrollTop: $statusBox.offset().top - 50
1920 }, 500);
1921 }
1922 },
1923
1924 /**
1925 * Handle Re-Index All button click
1926 */
1927 handleReindexAll: function(e) {
1928 e.preventDefault();
1929
1930 const $button = $(e.currentTarget);
1931 const confirmMessage = $button.data('confirm');
1932
1933 if (!confirm(confirmMessage)) {
1934 return;
1935 }
1936
1937 // Scroll to status section
1938 this.scrollToIndexingStatus();
1939
1940 // Check if we're in local storage mode
1941 if (this.isLocalStorageMode()) {
1942 // Use AJAX-driven batch processing for local mode
1943 this.startLocalIndexing($button);
1944 } else {
1945 // Use form submission for cloud mode
1946 this.submitRAGAction('reindex_all', $button);
1947 }
1948 },
1949
1950 /**
1951 * Handle Re-Index Topic Images button click
1952 * Only re-indexes topics that contain images
1953 */
1954 handleReindexImages: function(e) {
1955 e.preventDefault();
1956
1957 const $button = $(e.currentTarget);
1958 const confirmMessage = $button.data('confirm');
1959
1960 if (!confirm(confirmMessage)) {
1961 return;
1962 }
1963
1964 // Scroll to status section
1965 this.scrollToIndexingStatus();
1966
1967 // Check if we're in local storage mode
1968 if (this.isLocalStorageMode()) {
1969 // Use AJAX-driven batch processing for local mode with images_only flag
1970 this.startLocalIndexing($button, { images_only: true });
1971 } else {
1972 // Use form submission for cloud mode with images_only flag
1973 this.submitRAGAction('reindex_images', $button);
1974 }
1975 },
1976
1977 /**
1978 * Handle Clear Database button click
1979 */
1980 handleClearDatabase: function(e) {
1981 e.preventDefault();
1982
1983 const $button = $(e.currentTarget);
1984 const confirmMessage = 'WARNING: This will permanently delete all indexed data.\n\nType "DELETE" to confirm:';
1985
1986 const userInput = prompt(confirmMessage);
1987
1988 if (userInput !== 'DELETE') {
1989 if (userInput !== null) {
1990 alert('Confirmation failed. Database was not cleared.');
1991 }
1992 return;
1993 }
1994
1995 // Create and submit form with confirmation value
1996 this.submitRAGAction('clear_database', $button, { confirm: userInput });
1997 },
1998
1999 /**
2000 * Handle Clear & Re-Index button click
2001 */
2002 handleClearAndReindex: function(e) {
2003 e.preventDefault();
2004
2005 const $button = $(e.currentTarget);
2006 const confirmMessage = 'This will:\n1. Clear all indexed data\n2. Re-index all topics\n\nType "CONFIRM" to proceed:';
2007
2008 const userInput = prompt(confirmMessage);
2009
2010 if (userInput !== 'CONFIRM') {
2011 if (userInput !== null) {
2012 alert('Confirmation failed. Operation cancelled.');
2013 }
2014 return;
2015 }
2016
2017 // Check if we're in local storage mode
2018 if (this.isLocalStorageMode()) {
2019 // Use AJAX-driven process for local mode
2020 this.clearAndReindexLocal($button);
2021 } else {
2022 // Use form submission for cloud mode
2023 this.submitRAGAction('clear_and_reindex', $button);
2024 }
2025 },
2026
2027 /**
2028 * Clear and re-index for local storage mode via AJAX
2029 */
2030 clearAndReindexLocal: function($button) {
2031 const self = this;
2032
2033 // Show loading state
2034 $button.addClass('loading').prop('disabled', true);
2035 $button.html('<span class="dashicons dashicons-update wpforo-spin"></span> Clearing...');
2036
2037 // First clear local embeddings
2038 $.ajax({
2039 url: wpforoAIAdmin.ajaxUrl,
2040 type: 'POST',
2041 data: {
2042 action: 'wpforo_ai_action',
2043 wpforo_ai_action: 'clear_local_embeddings',
2044 _wpnonce: wpforoAIAdmin.nonce
2045 },
2046 success: function(response) {
2047 if (response.success) {
2048 console.log('Local embeddings cleared:', response.data);
2049 // Now start the indexing
2050 self.startLocalIndexing($button);
2051 } else {
2052 const errorMsg = response.data && response.data.message
2053 ? response.data.message
2054 : 'Failed to clear embeddings';
2055 alert('Error: ' + errorMsg);
2056 $button.removeClass('loading').prop('disabled', false);
2057 $button.html('<span class="dashicons dashicons-trash"></span> Clear & Re-Index');
2058 }
2059 },
2060 error: function(xhr, status, error) {
2061 console.error('Clear local embeddings error:', error);
2062 alert('Error clearing embeddings: ' + error);
2063 $button.removeClass('loading').prop('disabled', false);
2064 $button.html('<span class="dashicons dashicons-trash"></span> Clear & Re-Index');
2065 }
2066 });
2067 },
2068
2069 /**
2070 * Handle Stop Indexing button click
2071 */
2072 handleStopIndexing: function(e) {
2073 e.preventDefault();
2074
2075 const $button = $(e.currentTarget);
2076 const confirmMessage = $button.data('confirm');
2077
2078 if (!confirm(confirmMessage)) {
2079 return;
2080 }
2081
2082 // Set stopping flag so status shows "Stopping..." while process winds down
2083 // Use localStorage to persist across page reloads
2084 this.indexingStopping = true;
2085 localStorage.setItem('wpforo_indexing_stopping', 'true');
2086
2087 // Clear auto-refresh flag so page doesn't keep reloading after stop
2088 this.stopForumIndexingAutoRefresh();
2089
2090 // Immediately update status to show "Stopping..."
2091 const $statusElement = $('#rag-indexing-status');
2092 if ($statusElement.length) {
2093 $statusElement.text('Stopping...');
2094 }
2095
2096 // Check if we're in local storage mode with AJAX indexing
2097 if (this.isLocalStorageMode() && this.localIndexingState) {
2098 // Stop the AJAX-driven indexing loop (this updates UI)
2099 this.stopLocalIndexing();
2100 // Clear the queue on the server via AJAX (no page reload)
2101 this.clearLocalIndexingQueue();
2102 } else {
2103 // Cloud mode: tell the backend to stop the image_worker
2104 // draining queued media jobs. Polling will pick up the
2105 // drained state via the regular /rag/status poll.
2106 this.cancelCloudIndexing();
2107 }
2108 },
2109
2110 /**
2111 * Tell the backend to stop in-flight cloud indexing (image worker).
2112 * No page reload — polling will pick up the drained state.
2113 */
2114 cancelCloudIndexing: function() {
2115 const self = this;
2116 $.ajax({
2117 url: ajaxurl,
2118 type: 'POST',
2119 data: {
2120 action: 'wpforo_ai_cancel_cloud_indexing',
2121 _wpnonce: wpforoAIAdmin.nonce
2122 },
2123 success: function(response) {
2124 console.log('Cloud indexing cancel requested:', response);
2125 },
2126 error: function(xhr, status, error) {
2127 console.error('Failed to cancel cloud indexing:', error);
2128 // Clear the stopping flag so the user can retry
2129 self.indexingStopping = false;
2130 localStorage.removeItem('wpforo_indexing_stopping');
2131 }
2132 });
2133 },
2134
2135 /**
2136 * Handle "Cleanup Indexing Session" button clicks.
2137 *
2138 * Resets stuck indexing state (queues, WP-Cron jobs, transient locks,
2139 * status caches) without touching any already-indexed data. Works for
2140 * both local and cloud storage modes — the backend cleans up both
2141 * queue keys in one call and also tells the cloud image_worker to
2142 * drop any in-flight messages.
2143 *
2144 * Also clears the browser-side localStorage stopping flag so the UI
2145 * doesn't get stuck on "Stopping..." after the cleanup.
2146 *
2147 * data-scope on the button is 'forum' or 'wp'.
2148 */
2149 handleCleanupSession: function(e) {
2150 e.preventDefault();
2151 const $button = $(e.currentTarget);
2152 const scope = $button.data('scope') || 'forum';
2153 const confirmMsg = $button.data('confirm') || 'Reset stuck indexing session?';
2154
2155 if (!window.confirm(confirmMsg)) {
2156 return;
2157 }
2158
2159 const originalHtml = $button.html();
2160 $button.prop('disabled', true).html('<span class="dashicons dashicons-update"></span> Cleaning up...');
2161
2162 // Clear any browser-side stuck state first — regardless of AJAX
2163 // outcome. These are the client-side flags the plugin sets for
2164 // indexing (see handleStopIndexing / checkLocalIndexingProgress).
2165 try {
2166 localStorage.removeItem('wpforo_indexing_stopping');
2167 localStorage.removeItem('wpforo_wp_indexing_auto_refresh');
2168 localStorage.removeItem('wpforo_forum_indexing_auto_refresh');
2169 } catch (err) { /* localStorage may be blocked in some contexts */ }
2170 this.indexingStopping = false;
2171 this.stopWPIndexingAutoRefresh();
2172 this.stopForumIndexingAutoRefresh();
2173
2174 const self = this;
2175 $.ajax({
2176 url: ajaxurl,
2177 type: 'POST',
2178 data: {
2179 action: 'wpforo_ai_cleanup_indexing_session',
2180 scope: scope,
2181 _wpnonce: wpforoAIAdmin.nonce
2182 },
2183 success: function(response) {
2184 $button.prop('disabled', false).html(originalHtml);
2185 if (response && response.success) {
2186 // Reload to refresh all server-rendered counts and
2187 // flip the UI out of "Indexing..." state cleanly.
2188 window.location.reload();
2189 } else {
2190 const msg = (response && response.data && response.data.message) || 'Cleanup failed.';
2191 window.alert(msg);
2192 }
2193 },
2194 error: function(xhr, status, error) {
2195 $button.prop('disabled', false).html(originalHtml);
2196 console.error('Cleanup indexing session failed:', error);
2197 window.alert('Cleanup failed. Check the browser console for details.');
2198 }
2199 });
2200 },
2201
2202 /**
2203 * Clear local indexing queue via AJAX (no page reload)
2204 */
2205 clearLocalIndexingQueue: function() {
2206 $.ajax({
2207 url: ajaxurl,
2208 type: 'POST',
2209 data: {
2210 action: 'wpforo_ai_action',
2211 wpforo_ai_action: 'stop_local_indexing',
2212 _wpnonce: wpforoAIAdmin.nonce
2213 },
2214 success: function(response) {
2215 console.log('Local indexing queue cleared:', response);
2216 },
2217 error: function(xhr, status, error) {
2218 console.error('Failed to clear queue:', error);
2219 }
2220 });
2221 },
2222
2223 /**
2224 * Submit RAG action form
2225 */
2226 submitRAGAction: function(action, $button, additionalData) {
2227 // Create hidden form
2228 const $form = $('<form>', {
2229 method: 'post',
2230 action: ''
2231 });
2232
2233 // Add nonce - get from button's data-nonce attribute
2234 const nonceName = 'wpforo_ai_' + action;
2235 const nonceValue = $button.data('nonce'); // Get from button data attribute
2236
2237 $form.append($('<input>', {
2238 type: 'hidden',
2239 name: '_wpnonce',
2240 value: nonceValue
2241 }));
2242
2243 // Add action
2244 $form.append($('<input>', {
2245 type: 'hidden',
2246 name: 'wpforo_ai_action',
2247 value: action
2248 }));
2249
2250 // Add chunking configuration parameters for reindex actions
2251 if (action === 'reindex_all' || action === 'clear_and_reindex') {
2252 const chunkSize = $('#wpforo-ai-chunk-size').val() || 1000;
2253 const overlapPercent = $('#wpforo-ai-overlap-percent').val() || 20;
2254
2255 $form.append($('<input>', {
2256 type: 'hidden',
2257 name: 'chunk_size',
2258 value: chunkSize
2259 }));
2260
2261 $form.append($('<input>', {
2262 type: 'hidden',
2263 name: 'overlap_percent',
2264 value: overlapPercent
2265 }));
2266 }
2267
2268 // Add additional data if provided
2269 if (additionalData) {
2270 $.each(additionalData, function(key, value) {
2271 $form.append($('<input>', {
2272 type: 'hidden',
2273 name: key,
2274 value: value
2275 }));
2276 });
2277 }
2278
2279 // Add loading state to button
2280 $button.addClass('loading').prop('disabled', true);
2281
2282 // Set localStorage flag for reindex actions so auto-refresh survives page reloads
2283 if (action === 'reindex_all' || action === 'clear_and_reindex' || action === 'reindex_images') {
2284 try {
2285 localStorage.setItem('wpforo_forum_indexing_auto_refresh', '1');
2286 } catch (e) { /* localStorage may be blocked */ }
2287 }
2288
2289 // Append form to body and submit
2290 $('body').append($form);
2291 $form.submit();
2292 },
2293
2294 /**
2295 * Refresh RAG status via AJAX
2296 */
2297 refreshRAGStatus: function() {
2298 const self = this;
2299
2300 $.ajax({
2301 url: ajaxurl,
2302 type: 'POST',
2303 data: {
2304 action: 'wpforo_ai_get_rag_status',
2305 _wpnonce: self.ajaxNonce || $('#_wpnonce').val()
2306 },
2307 success: function(response) {
2308 if (response.success && response.data) {
2309 self.updateRAGStatusDisplay(response.data);
2310 }
2311 },
2312 error: function(xhr, status, error) {
2313 console.error('Failed to refresh RAG status:', error);
2314 }
2315 });
2316 },
2317
2318 /**
2319 * Update RAG status display
2320 */
2321 updateRAGStatusDisplay: function(data) {
2322 // Update total topics indexed (threads) count - sync all displays
2323 if (typeof data.total_topics !== 'undefined') {
2324 const formattedTopics = this.formatNumber(data.total_topics);
2325 $('#rag-total-topics').text(formattedTopics);
2326 $('#local-total-topics').text(formattedTopics);
2327 $('#index-total-indexed').text(formattedTopics);
2328
2329 // Update remaining to index
2330 const $totalTopicsCount = $('#index-total-topics-count');
2331 if ($totalTopicsCount.length) {
2332 const totalCount = parseInt($totalTopicsCount.text().replace(/,/g, ''), 10) || 0;
2333 const indexed = data.total_topics;
2334 const remaining = Math.max(0, totalCount - indexed);
2335 const $remainingEl = $('#index-remaining');
2336 $remainingEl.text(this.formatNumber(remaining));
2337 if (remaining === 0) {
2338 $remainingEl.addClass('stat-success');
2339 } else {
2340 $remainingEl.removeClass('stat-success');
2341 }
2342 }
2343 }
2344
2345 // Update local storage stats if available
2346 if (typeof data.total_indexed !== 'undefined') {
2347 $('#local-total-embeddings').text(this.formatNumber(data.total_indexed));
2348 }
2349 if (typeof data.storage_size_mb !== 'undefined') {
2350 $('#local-storage-size').text(data.storage_size_mb + ' MB');
2351 }
2352
2353 // Update credits if available in response
2354 if (typeof data.credits_remaining !== 'undefined') {
2355 $('#index-credits-available').text(this.formatNumber(data.credits_remaining));
2356 }
2357
2358 // Update indexing status — only show spinner when backend is actively indexing
2359 // or a cron batch is actively running. Queued topics with a future schedule
2360 // (e.g. 24h auto-indexing delay) should NOT trigger the spinner.
2361 if (typeof data.is_indexing !== 'undefined') {
2362 const $statusElement = $('#rag-indexing-status');
2363 const $statusIcon = $statusElement.closest('.rag-stat-item').find('.dashicons');
2364 const cronActive = data.pending_cron_jobs && data.pending_cron_jobs.is_actively_processing;
2365 const isActivelyProcessing = data.is_indexing || cronActive;
2366
2367 // Track previous state to detect completion
2368 const wasProcessing = this.previousProcessingState === true;
2369 this.previousProcessingState = isActivelyProcessing;
2370
2371 if (isActivelyProcessing) {
2372 // Show indexing state
2373 let statusText;
2374 if (this.indexingStopping) {
2375 statusText = 'Stopping...';
2376 } else {
2377 statusText = 'Indexing...';
2378 }
2379 $statusElement
2380 .text(statusText)
2381 .removeClass('status-idle')
2382 .addClass('status-active');
2383 $statusIcon
2384 .removeClass('dashicons-saved')
2385 .addClass('dashicons-update-alt wpforo-rag-status-spin');
2386 } else {
2387 // Clear stopping flag when process is fully stopped
2388 this.indexingStopping = false;
2389 localStorage.removeItem('wpforo_indexing_stopping');
2390
2391 $statusElement
2392 .text('Idle')
2393 .removeClass('status-active')
2394 .addClass('status-idle');
2395 $statusIcon
2396 .removeClass('dashicons-update-alt wpforo-rag-status-spin')
2397 .addClass('dashicons-saved');
2398
2399 // Stop polling when backend is no longer indexing
2400 this.stopRAGStatusPolling();
2401
2402 // Reload page when processing completes to refresh all counts
2403 if (wasProcessing) {
2404 setTimeout(function() {
2405 window.location.reload();
2406 }, 1000);
2407 }
2408 }
2409 }
2410
2411 // Update queued topics count in the "Total Threads Indexed" stat
2412 if (data.pending_cron_jobs) {
2413 const pendingTopics = data.pending_cron_jobs.pending_topics || 0;
2414 const $queuedCount = $('#rag-total-topics .rag-queued-count');
2415 if (pendingTopics > 0) {
2416 if ($queuedCount.length) {
2417 $queuedCount.text('| ' + this.formatNumber(pendingTopics) + ' queued...');
2418 } else {
2419 $('#rag-total-topics').append(' <small class="rag-queued-count">| ' + this.formatNumber(pendingTopics) + ' queued...</small>');
2420 }
2421 } else {
2422 $queuedCount.remove();
2423 }
2424 }
2425
2426 // Update queue info
2427 if (typeof data.queue_info !== 'undefined') {
2428 $('#rag-queue-pending').text(data.queue_info.pending || 0);
2429 $('#rag-queue-processing').text(data.queue_info.processing || 0);
2430 $('#rag-queue-failed').text(data.queue_info.failed || 0);
2431
2432 // Show/hide queue info box
2433 if (data.is_indexing) {
2434 $('.wpforo-ai-queue-info').show();
2435 } else {
2436 $('.wpforo-ai-queue-info').hide();
2437 }
2438 }
2439
2440 // Async media (image/document) sub-progress.
2441 // Present when the backend image_worker has queued work. The
2442 // element is created on demand and lives inside the queue-info
2443 // box so it inherits existing styling.
2444 this.renderMediaProgress(data.media_progress);
2445
2446 // Update last indexed timestamp
2447 if (typeof data.last_indexed_at !== 'undefined' && data.last_indexed_at) {
2448 $('#rag-last-indexed').text(data.last_indexed_at);
2449 }
2450 },
2451
2452 /**
2453 * Render the async media (image/document) sub-progress line.
2454 *
2455 * The backend image_worker processes images and documents out-of-band
2456 * from text ingestion. This function creates (on first call) and
2457 * updates a small status line showing "Media: done/total processed"
2458 * inside the existing queue-info box. When no media work is in
2459 * flight the element is hidden.
2460 *
2461 * @param {Object|null} mediaProgress {total, done, failed, skipped_cancelled, in_flight, progress_percent}
2462 */
2463 renderMediaProgress: function(mediaProgress) {
2464 const $container = $('#wpforo-ai-queue-info');
2465 const $existing = $('#rag-media-progress');
2466
2467 if (!mediaProgress || !mediaProgress.total) {
2468 $existing.hide();
2469 return;
2470 }
2471
2472 let $el = $existing;
2473 if (!$el.length) {
2474 if (!$container.length) {
2475 return;
2476 }
2477 $el = $('<div id="rag-media-progress" class="wpforo-ai-media-progress"></div>');
2478 $container.append($el);
2479 }
2480
2481 const done = parseInt(mediaProgress.done, 10) || 0;
2482 const total = parseInt(mediaProgress.total, 10) || 0;
2483 const failed = parseInt(mediaProgress.failed, 10) || 0;
2484 const skipped = parseInt(mediaProgress.skipped_cancelled, 10) || 0;
2485 const percent = parseInt(mediaProgress.progress_percent, 10) || 0;
2486
2487 // Hardcoded English to match surrounding status strings
2488 // ('Indexing...', 'Stopping...', 'Idle'). No JS i18n layer here.
2489 const label = mediaProgress.in_flight ? 'Processing media' : 'Media processed';
2490
2491 let line = label + ': ' + done + ' / ' + total + ' (' + percent + '%)';
2492 if (failed > 0) {
2493 line += '' + failed + ' failed';
2494 }
2495 if (skipped > 0) {
2496 line += '' + skipped + ' skipped';
2497 }
2498
2499 $el.text(line).show();
2500 },
2501
2502 /**
2503 * Start polling for RAG status updates
2504 */
2505 startRAGStatusPolling: function() {
2506 const self = this;
2507
2508 // Initialize state tracking - assume processing is active when polling starts
2509 this.previousProcessingState = true;
2510
2511 // Poll every 10 seconds while processing is active
2512 this.ragStatusInterval = setInterval(function() {
2513 self.refreshRAGStatus();
2514 }, 10000);
2515
2516 // Safety timeout after 2 hours (in case of stuck state)
2517 // Normal completion will stop polling via stopRAGStatusPolling() when processing completes
2518 this.ragSafetyTimeout = setTimeout(function() {
2519 console.log('RAG polling safety timeout reached (2 hours). Stopping polling.');
2520 self.stopRAGStatusPolling();
2521 // Reload page to get fresh state
2522 window.location.reload();
2523 }, 7200000); // 2 hours
2524 },
2525
2526 /**
2527 * Stop polling for RAG status updates
2528 */
2529 stopRAGStatusPolling: function() {
2530 if (this.ragStatusInterval) {
2531 clearInterval(this.ragStatusInterval);
2532 this.ragStatusInterval = null;
2533 }
2534 // Also clear safety timeout if it exists
2535 if (this.ragSafetyTimeout) {
2536 clearTimeout(this.ragSafetyTimeout);
2537 this.ragSafetyTimeout = null;
2538 }
2539 },
2540
2541 /**
2542 * Start auto page refresh for forum content indexing (cloud mode).
2543 * Sets localStorage flag so polling survives page reloads.
2544 */
2545 startForumIndexingAutoRefresh: function() {
2546 try {
2547 localStorage.setItem('wpforo_forum_indexing_auto_refresh', '1');
2548 } catch (e) { /* localStorage may be blocked */ }
2549
2550 console.log('Forum indexing: reloading page to start auto-refresh...');
2551 window.location.hash = 'rag-status-section';
2552 window.location.reload();
2553 },
2554
2555 /**
2556 * Check on page load if forum indexing auto-refresh should continue.
2557 * Polls API and schedules next reload if still indexing.
2558 */
2559 checkForumIndexingAutoRefresh: function() {
2560 const self = this;
2561
2562 let inAutoRefresh = false;
2563 try {
2564 inAutoRefresh = localStorage.getItem('wpforo_forum_indexing_auto_refresh') === '1';
2565 } catch (e) { /* localStorage may be blocked */ }
2566
2567 if (!inAutoRefresh) {
2568 return;
2569 }
2570
2571 $.ajax({
2572 url: ajaxurl,
2573 type: 'POST',
2574 data: {
2575 action: 'wpforo_ai_get_rag_status',
2576 _wpnonce: wpforoAIAdmin.nonce
2577 },
2578 success: function(response) {
2579 if (response.success && response.data) {
2580 const cronActive = response.data.pending_cron_jobs && response.data.pending_cron_jobs.is_actively_processing;
2581 const hasPendingJobs = response.data.pending_cron_jobs && response.data.pending_cron_jobs.has_pending_jobs;
2582 const isActivelyProcessing = response.data.is_indexing || cronActive || hasPendingJobs;
2583
2584 if (isActivelyProcessing) {
2585 console.log('Forum indexing in progress, will refresh in 20 seconds...');
2586 self._forumAutoRefreshTimeout = setTimeout(function() {
2587 window.location.hash = 'rag-status-section';
2588 window.location.reload();
2589 }, 20000);
2590 } else {
2591 console.log('Forum indexing complete, final reload');
2592 self.stopForumIndexingAutoRefresh();
2593 window.location.hash = 'rag-status-section';
2594 window.location.reload();
2595 }
2596 }
2597 },
2598 error: function() {
2599 self.stopForumIndexingAutoRefresh();
2600 }
2601 });
2602 },
2603
2604 /**
2605 * Stop forum indexing auto page refresh and clear the flag.
2606 */
2607 stopForumIndexingAutoRefresh: function() {
2608 try {
2609 localStorage.removeItem('wpforo_forum_indexing_auto_refresh');
2610 } catch (e) { /* localStorage may be blocked */ }
2611
2612 if (this._forumAutoRefreshTimeout) {
2613 clearTimeout(this._forumAutoRefreshTimeout);
2614 this._forumAutoRefreshTimeout = null;
2615 }
2616 },
2617
2618 /**
2619 * Stop WP indexing auto refresh (stub for cleanup handler).
2620 * The actual implementation lives in ai-features-wp-indexing.js.
2621 */
2622 stopWPIndexingAutoRefresh: function() {
2623 try {
2624 localStorage.removeItem('wpforo_wp_indexing_auto_refresh');
2625 } catch (e) { /* localStorage may be blocked */ }
2626 },
2627
2628 /**
2629 * Handle search test form submission
2630 */
2631 handleSearchTest: function(e) {
2632 e.preventDefault();
2633
2634 const self = this;
2635 const $form = $(e.currentTarget);
2636 const $button = $form.find('#search-test-btn');
2637 const $spinner = $form.find('.spinner');
2638 const $results = $('#search-test-results');
2639 const $resultsContent = $('#search-results-content');
2640
2641 const query = $form.find('#search-query').val().trim();
2642 const limit = parseInt($form.find('#search-limit').val()) || 5;
2643
2644 if (!query) {
2645 alert('Please enter a search query.');
2646 return;
2647 }
2648
2649 // Show loading state with "Searching..." text
2650 $button.prop('disabled', true).addClass('loading');
2651 $button.html('<span class="dashicons dashicons-search"></span> Searching...');
2652 $spinner.addClass('is-active');
2653 $results.hide();
2654 $resultsContent.html('');
2655
2656 // Perform AJAX search
2657 $.ajax({
2658 url: ajaxurl,
2659 type: 'POST',
2660 data: {
2661 action: 'wpforo_ai_semantic_search',
2662 _wpnonce: self.ajaxNonce || $('#wpforo-ai-search-test-form #_wpnonce').val(),
2663 query: query,
2664 limit: limit
2665 },
2666 success: function(response) {
2667 if (response.success && response.data) {
2668 self.displaySearchResults(response.data, query);
2669 } else {
2670 const errorMsg = response.data && response.data.message
2671 ? response.data.message
2672 : 'Search failed. Please try again.';
2673 $resultsContent.html('<div class="notice notice-error"><p>' + errorMsg + '</p></div>');
2674 $results.show();
2675 }
2676 },
2677 error: function(xhr, status, error) {
2678 console.error('Search error:', error, xhr);
2679
2680 // Try to extract the actual error message from the response
2681 let errorMsg = 'Search request failed: ' + error;
2682 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2683 errorMsg = xhr.responseJSON.data.message;
2684 }
2685
2686 $resultsContent.html('<div class="notice notice-error"><p>' + errorMsg + '</p></div>');
2687 $results.show();
2688 },
2689 complete: function() {
2690 // Always reset button state, even on error
2691 $button.prop('disabled', false);
2692 $button.removeClass('loading');
2693 $button.html('<span class="dashicons dashicons-search"></span> Test Search');
2694 $spinner.removeClass('is-active');
2695 }
2696 });
2697 },
2698
2699 /**
2700 * Display search results
2701 */
2702 displaySearchResults: function(data, query) {
2703 const $resultsContent = $('#search-results-content');
2704 const $results = $('#search-test-results');
2705
2706 // Clear previous results
2707 $resultsContent.html('');
2708
2709 // Show query info
2710 const queryInfo = $('<div class="search-query-info">')
2711 .append($('<p>').html(
2712 '<strong>Query:</strong> "' + this.escapeHtml(query) + '" | ' +
2713 '<strong>Results:</strong> ' + data.total + ' found | ' +
2714 '<strong>Time:</strong> ' + data.query_time_ms + 'ms'
2715 ));
2716 $resultsContent.append(queryInfo);
2717
2718 // Show credit status if available
2719 if (data.credit_status && data.credit_status.credits) {
2720 const creditInfo = $('<div class="search-credit-info notice notice-info inline">')
2721 .append($('<p>').html(
2722 '<strong>Credits Remaining:</strong> ' + data.credit_status.credits.remaining + ' / ' + data.credit_status.credits.total +
2723 ' (' + data.credit_status.credits.usage_percent.toFixed(1) + '% used)'
2724 ));
2725 $resultsContent.append(creditInfo);
2726 }
2727
2728 // Display results
2729 if (data.results && data.results.length > 0) {
2730 const $resultsList = $('<div class="search-results-list">');
2731
2732 data.results.forEach(function(result, index) {
2733 const $resultItem = $('<div class="search-result-item">');
2734
2735 // Result header with rank and score
2736 $resultItem.append(
2737 $('<div class="result-header">').html(
2738 '<strong>#' + (index + 1) + '</strong> - Score: ' + (result.score * 100).toFixed(1) + '%'
2739 )
2740 );
2741
2742 // Title and excerpt
2743 $resultItem.append($('<h4 class="result-title">').text(result.title));
2744 $resultItem.append($('<p class="result-excerpt">').text(result.excerpt));
2745
2746 // Generate post-specific URL if chunk_post_id is available
2747 let postUrl = result.url; // Default to topic URL
2748 if (result.metadata && result.metadata.chunk_post_id) {
2749 // Build post-specific URL: /community/postid/{id}/
2750 // Extract base forum URL from topic_url
2751 const topicUrl = result.metadata.topic_url || result.url;
2752 if (topicUrl) {
2753 // Extract everything up to and including /community/
2754 const match = topicUrl.match(/^(.*\/community\/)/);
2755 if (match) {
2756 const baseUrl = match[1];
2757 postUrl = baseUrl + 'postid/' + result.metadata.chunk_post_id + '/';
2758 }
2759 }
2760 }
2761
2762 // URL (show post-specific URL if available, otherwise topic URL)
2763 if (postUrl) {
2764 const urlLabel = result.metadata && result.metadata.chunk_post_id ? 'Post URL' : 'Topic URL';
2765 $resultItem.append(
2766 $('<p class="result-url">').html(
2767 '<strong>' + urlLabel + ':</strong> ' +
2768 '<a href="' + this.escapeHtml(postUrl) + '" target="_blank" class="button button-small">' +
2769 'View Post →</a> ' +
2770 '<code style="margin-left: 10px;">' + this.escapeHtml(postUrl) + '</code>'
2771 )
2772 );
2773 }
2774
2775 // Metadata as formatted JSON
2776 if (result.metadata) {
2777 const $metadataBox = $('<div class="result-metadata">');
2778 $metadataBox.append($('<strong>').text('Metadata:'));
2779 $metadataBox.append($('<pre>').text(JSON.stringify(result.metadata, null, 2)));
2780 $resultItem.append($metadataBox);
2781 }
2782
2783 // Full result JSON (collapsible) - only show in debug mode
2784 if (typeof wpforoAIAdmin !== 'undefined' && wpforoAIAdmin.debugMode) {
2785 const $fullJsonToggle = $('<button class="button button-small toggle-json-btn" type="button">')
2786 .text('Show Full JSON')
2787 .on('click', function() {
2788 const $this = $(this);
2789 const $jsonBox = $this.next('.result-full-json');
2790 if ($jsonBox.is(':visible')) {
2791 $jsonBox.hide();
2792 $this.text('Show Full JSON');
2793 } else {
2794 $jsonBox.show();
2795 $this.text('Hide Full JSON');
2796 }
2797 });
2798
2799 const $fullJson = $('<div class="result-full-json" style="display:none;">');
2800 $fullJson.append($('<pre>').text(JSON.stringify(result, null, 2)));
2801
2802 $resultItem.append($fullJsonToggle);
2803 $resultItem.append($fullJson);
2804 }
2805
2806 $resultsList.append($resultItem);
2807 }.bind(this));
2808
2809 $resultsContent.append($resultsList);
2810 } else {
2811 $resultsContent.append(
2812 $('<div class="notice notice-warning"><p>No results found for your query.</p></div>')
2813 );
2814 }
2815
2816 // Show results container
2817 $results.show();
2818 },
2819
2820 // =====================================================
2821 // Local Storage AJAX-Driven Indexing
2822 // =====================================================
2823
2824 /**
2825 * Check if we're in local storage mode
2826 */
2827 isLocalStorageMode: function() {
2828 const $localRadio = $('input[name="wpforo_ai_storage_mode"][value="local"]');
2829 // If radio buttons don't exist (cloud storage feature not available),
2830 // the storage mode is always local (default) — return true
2831 if (!$localRadio.length) {
2832 return true;
2833 }
2834 return $localRadio.is(':checked');
2835 },
2836
2837 /**
2838 * Start local indexing process via AJAX
2839 * @param {jQuery} $button - The button that triggered the indexing
2840 * @param {Object} options - Optional parameters (images_only: bool)
2841 */
2842 startLocalIndexing: function($button, options) {
2843 options = options || {};
2844
2845 // Get settings from the form (pagination_size is used as batch size)
2846 const chunkSize = $('#wpforo-ai-chunk-size').val() || 512;
2847 const overlapPercent = $('#wpforo-ai-overlap-percent').val() || 20;
2848 const batchSize = $('#wpforo-ai-pagination-size').val() || 10;
2849
2850 // Store original button HTML for restoration later
2851 if (!$button.data('original-html')) {
2852 $button.data('original-html', $button.html());
2853 }
2854
2855 // Show loading state
2856 $button.addClass('loading').prop('disabled', true);
2857 $button.html('<span class="dashicons dashicons-update wpforo-spin"></span> Starting...');
2858
2859 // Build AJAX data
2860 const ajaxData = {
2861 action: 'wpforo_ai_action',
2862 wpforo_ai_action: 'start_local_indexing',
2863 _wpnonce: wpforoAIAdmin.nonce,
2864 chunk_size: chunkSize,
2865 overlap_percent: overlapPercent,
2866 batch_size: batchSize
2867 };
2868
2869 // Add images_only flag if set
2870 if (options.images_only) {
2871 ajaxData.images_only = 1;
2872 }
2873
2874 // Call the start_local_indexing AJAX action
2875 $.ajax({
2876 url: wpforoAIAdmin.ajaxUrl,
2877 type: 'POST',
2878 data: ajaxData,
2879 success: function(response) {
2880 if (response.success) {
2881 console.log('Local indexing started:', response.data);
2882
2883 // Use auto-refresh mechanism for consistent UX with cloud mode.
2884 // Sets localStorage flag and reloads; checkForumIndexingAutoRefresh()
2885 // will continue refreshing every 20s while indexing is in progress.
2886 WpForoAI.startForumIndexingAutoRefresh();
2887 } else {
2888 const errorMsg = response.data && response.data.message
2889 ? response.data.message
2890 : 'Failed to start indexing';
2891 alert('Error: ' + errorMsg);
2892 $button.removeClass('loading').prop('disabled', false).show();
2893 if ($button.data('original-html')) {
2894 $button.html($button.data('original-html'));
2895 }
2896 }
2897 },
2898 error: function(xhr, status, error) {
2899 console.error('Start local indexing error:', error);
2900 alert('Error starting indexing: ' + error);
2901 $button.removeClass('loading').prop('disabled', false).show();
2902 if ($button.data('original-html')) {
2903 $button.html($button.data('original-html'));
2904 }
2905 }
2906 });
2907 },
2908
2909 /**
2910 * Process local indexing batches in a loop
2911 */
2912 processLocalBatches: function($button) {
2913 const self = this;
2914
2915 // Guard against concurrent calls (e.g., page reload while previous request in-flight)
2916 if (this._batchProcessing) {
2917 return;
2918 }
2919
2920 // Check if indexing was stopped
2921 if (this.localIndexingStopped) {
2922 this.localIndexingStopped = false;
2923 // Buttons already reset by stopLocalIndexing()
2924 return;
2925 }
2926
2927 this._batchProcessing = true;
2928
2929 // Call the process_local_batch AJAX action
2930 $.ajax({
2931 url: wpforoAIAdmin.ajaxUrl,
2932 type: 'POST',
2933 data: {
2934 action: 'wpforo_ai_action',
2935 wpforo_ai_action: 'process_local_batch',
2936 _wpnonce: wpforoAIAdmin.nonce
2937 },
2938 success: function(response) {
2939 self._batchProcessing = false;
2940
2941 if (response.success) {
2942 const data = response.data;
2943 console.log('Batch processed:', data);
2944
2945 // Handle 'wait' action — another process is indexing, retry
2946 if (data.action === 'wait') {
2947 setTimeout(function() {
2948 self.processLocalBatches($button);
2949 }, 2000);
2950 return;
2951 }
2952
2953 // Update state
2954 self.localIndexingState.processed = data.processed;
2955 self.localIndexingState.remaining = data.remaining;
2956
2957 if (data.errors && data.errors.length > 0) {
2958 self.localIndexingState.errors = self.localIndexingState.errors.concat(data.errors);
2959 }
2960
2961 // Update UI
2962 self.updateLocalIndexingUI(data);
2963
2964 // Check if done
2965 if (data.done) {
2966 self.finishLocalIndexing($button, data);
2967 } else {
2968 // Continue processing next batch after a short delay
2969 setTimeout(function() {
2970 self.processLocalBatches($button);
2971 }, 500); // 500ms delay between batches
2972 }
2973 } else {
2974 const errorMsg = response.data && response.data.message
2975 ? response.data.message
2976 : 'Batch processing failed';
2977 console.error('Batch error:', errorMsg);
2978
2979 // Check for credits exhausted - stop immediately
2980 if (response.data && (response.data.action === 'credits_exhausted' || (errorMsg && errorMsg.indexOf('nsufficient credits') !== -1))) {
2981 self.localIndexingState.processed = response.data.processed || 0;
2982 self.localIndexingState.remaining = 0;
2983 self.updateLocalIndexingUI(response.data);
2984 self.finishLocalIndexing($button, {
2985 errors: [errorMsg],
2986 credits_exhausted: true
2987 });
2988 return;
2989 }
2990
2991 // Try to continue if there are remaining items
2992 if (self.localIndexingState.remaining > 0) {
2993 self.localIndexingState.errors.push(errorMsg);
2994 self.updateLocalIndexingUI(self.localIndexingState);
2995 setTimeout(function() {
2996 self.processLocalBatches($button);
2997 }, 1000);
2998 } else {
2999 self.finishLocalIndexing($button, { errors: [errorMsg] });
3000 }
3001 }
3002 },
3003 error: function(xhr, status, error) {
3004 self._batchProcessing = false;
3005 console.error('Process batch error:', error);
3006
3007 // Check response body for credits_exhausted
3008 try {
3009 var responseData = xhr.responseJSON || JSON.parse(xhr.responseText || '{}');
3010 if (responseData.data && responseData.data.action === 'credits_exhausted') {
3011 self.finishLocalIndexing($button, {
3012 errors: [responseData.data.message || 'Insufficient credits'],
3013 credits_exhausted: true
3014 });
3015 return;
3016 }
3017 } catch(e) {}
3018
3019 // Retry after a delay if there are remaining items
3020 if (self.localIndexingState.remaining > 0) {
3021 self.localIndexingState.errors.push('Network error: ' + error);
3022 setTimeout(function() {
3023 self.processLocalBatches($button);
3024 }, 2000);
3025 } else {
3026 self.finishLocalIndexing($button, { errors: ['Network error: ' + error] });
3027 }
3028 }
3029 });
3030 },
3031
3032 /**
3033 * Show local indexing progress UI
3034 */
3035 showLocalIndexingProgress: function() {
3036 const state = this.localIndexingState;
3037
3038 // Create or update progress container
3039 let $progress = $('#wpforo-local-indexing-progress');
3040 if (!$progress.length) {
3041 $progress = $('<div id="wpforo-local-indexing-progress" class="notice notice-info">' +
3042 '<p><strong>Local Indexing in Progress</strong></p>' +
3043 '<div class="progress-bar-container" style="width: 100%; height: 20px; background: #e0e0e0; border-radius: 4px; overflow: hidden;">' +
3044 '<div class="progress-bar" style="width: 0%; height: 100%; background: #0073aa; transition: width 0.3s;"></div>' +
3045 '</div>' +
3046 '<p class="progress-text">Processed: <span class="processed">0</span> / <span class="total">' + state.total + '</span> topics</p>' +
3047 '<p class="error-text" style="color: #d63638; display: none;">Errors: <span class="error-count">0</span></p>' +
3048 '</div>');
3049
3050 // Insert before the action buttons
3051 $('.wpforo-ai-bulk-actions').before($progress);
3052 }
3053
3054 $progress.find('.total').text(state.total);
3055 $progress.show();
3056
3057 // Update status indicator (same as cloud indexing)
3058 const $statusElement = $('#rag-indexing-status');
3059 const $statusIcon = $statusElement.closest('.rag-stat-item').find('.dashicons');
3060 $statusElement
3061 .text('Indexing...')
3062 .removeClass('status-idle')
3063 .addClass('status-active');
3064 $statusIcon
3065 .removeClass('dashicons-saved')
3066 .addClass('dashicons-update-alt wpforo-rag-status-spin');
3067
3068 // Show existing stop button, hide reindex button
3069 $('.wpforo-ai-reindex-all').hide();
3070 $('.wpforo-ai-stop-indexing').show();
3071 },
3072
3073 /**
3074 * Update local indexing progress UI
3075 */
3076 updateLocalIndexingUI: function(data) {
3077 const state = this.localIndexingState;
3078 const $progress = $('#wpforo-local-indexing-progress');
3079
3080 if (!$progress.length) return;
3081
3082 const processed = data.processed || state.processed;
3083 const total = state.total;
3084 const percent = total > 0 ? Math.round((processed / total) * 100) : 0;
3085
3086 $progress.find('.progress-bar').css('width', percent + '%');
3087 $progress.find('.processed').text(this.formatNumber(processed));
3088 $progress.find('.total').text(this.formatNumber(total));
3089
3090 // Show errors if any
3091 const errorCount = state.errors.length;
3092 if (errorCount > 0) {
3093 $progress.find('.error-text').show().find('.error-count').text(errorCount);
3094 }
3095
3096 // Update stats on the page
3097 const remaining = total - processed;
3098 $('#index-remaining').text(this.formatNumber(remaining));
3099 $('#index-total-indexed').text(this.formatNumber(processed));
3100 $('#rag-total-topics').text(this.formatNumber(processed));
3101
3102 // Update credits if available
3103 if (typeof data.credits_remaining !== 'undefined') {
3104 $('#index-credits-available').text(this.formatNumber(data.credits_remaining));
3105 }
3106 },
3107
3108 /**
3109 * Finish local indexing
3110 */
3111 finishLocalIndexing: function($button, data) {
3112 const self = this;
3113 const state = this.localIndexingState;
3114 const $progress = $('#wpforo-local-indexing-progress');
3115
3116 // Calculate elapsed time
3117 const elapsed = Date.now() - state.startTime;
3118 const elapsedSeconds = Math.round(elapsed / 1000);
3119 const minutes = Math.floor(elapsedSeconds / 60);
3120 const seconds = elapsedSeconds % 60;
3121 const timeStr = minutes > 0 ? minutes + 'm ' + seconds + 's' : seconds + 's';
3122
3123 // Show completion message
3124 if (data && data.credits_exhausted) {
3125 $progress.removeClass('notice-info').addClass('notice-error');
3126 $progress.find('p:first strong').text('Indexing Stopped - Insufficient Credits');
3127 $progress.find('.progress-text').html(
3128 'Indexed ' + this.formatNumber(state.processed) + ' of ' + this.formatNumber(state.total) +
3129 ' topics. <strong style="color: #d63638;">Please wait for your monthly credit reset or purchase additional credits to continue.</strong>'
3130 );
3131 } else if (state.errors.length > 0) {
3132 $progress.removeClass('notice-info').addClass('notice-warning');
3133 $progress.find('p:first strong').text('Indexing Complete with Errors');
3134 $progress.find('.progress-text').html(
3135 'Processed: ' + this.formatNumber(state.processed) + ' / ' + this.formatNumber(state.total) +
3136 ' topics in ' + timeStr + '. ' +
3137 '<strong style="color: #d63638;">' + state.errors.length + ' errors occurred.</strong>'
3138 );
3139 } else {
3140 $progress.removeClass('notice-info').addClass('notice-success');
3141 $progress.find('p:first strong').text('Indexing Complete!');
3142 $progress.find('.progress-text').html(
3143 'Successfully indexed ' + this.formatNumber(state.processed) + ' topics in ' + timeStr + '.'
3144 );
3145 $progress.find('.progress-bar').css('background', '#00a32a');
3146 }
3147
3148 // Show reindex button, hide stop button, restore original button text
3149 $('.wpforo-ai-stop-indexing').hide();
3150 const $reindexBtn = $('.wpforo-ai-reindex-all');
3151 $reindexBtn.show().removeClass('loading').prop('disabled', false);
3152 if ($reindexBtn.data('original-html')) {
3153 $reindexBtn.html($reindexBtn.data('original-html'));
3154 }
3155
3156 // Update status indicator (same as cloud indexing)
3157 const $statusElement = $('#rag-indexing-status');
3158 const $statusIcon = $statusElement.closest('.rag-stat-item').find('.dashicons');
3159 $statusElement
3160 .text('Idle')
3161 .removeClass('status-active')
3162 .addClass('status-idle');
3163 $statusIcon
3164 .removeClass('dashicons-update-alt wpforo-rag-status-spin')
3165 .addClass('dashicons-saved');
3166
3167 // Refresh stats after a short delay
3168 setTimeout(function() {
3169 self.refreshRAGStatus();
3170 }, 1000);
3171
3172 // Auto-hide progress after 10 seconds
3173 setTimeout(function() {
3174 $progress.fadeOut(500, function() {
3175 $(this).remove();
3176 });
3177 }, 10000);
3178 },
3179
3180 /**
3181 * Stop local indexing
3182 */
3183 stopLocalIndexing: function() {
3184 this.localIndexingStopped = true;
3185
3186 // Show reindex button, hide stop button, restore original button text
3187 $('.wpforo-ai-stop-indexing').hide();
3188 const $reindexBtn = $('.wpforo-ai-reindex-all');
3189 $reindexBtn.show().removeClass('loading').prop('disabled', false);
3190 if ($reindexBtn.data('original-html')) {
3191 $reindexBtn.html($reindexBtn.data('original-html'));
3192 }
3193
3194 // Show "Stopping..." status while background jobs complete
3195 // The status will change to "Idle" when updateRAGStatusDisplay detects no more pending jobs
3196 const $statusElement = $('#rag-indexing-status');
3197 $statusElement.text('Stopping...');
3198
3199 // Update progress UI
3200 const $progress = $('#wpforo-local-indexing-progress');
3201 if ($progress.length) {
3202 $progress.removeClass('notice-info').addClass('notice-warning');
3203 $progress.find('p:first strong').text('Indexing Stopped');
3204 $progress.find('.progress-text').html('Indexing was stopped by user.');
3205
3206 setTimeout(function() {
3207 $progress.fadeOut(500, function() {
3208 $(this).remove();
3209 });
3210 }, 5000);
3211 }
3212 },
3213
3214 /**
3215 * Check for in-progress local indexing on page load (auto-resume)
3216 */
3217 checkLocalIndexingProgress: function() {
3218 const self = this;
3219
3220 // Only check if we're on the AI features page and in local mode
3221 if (!this.isLocalStorageMode()) {
3222 return;
3223 }
3224
3225 $.ajax({
3226 url: wpforoAIAdmin.ajaxUrl,
3227 type: 'POST',
3228 data: {
3229 action: 'wpforo_ai_action',
3230 wpforo_ai_action: 'get_indexing_progress',
3231 _wpnonce: wpforoAIAdmin.nonce
3232 },
3233 success: function(response) {
3234 if (response.success && response.data.indexing_active) {
3235 console.log('Found in-progress indexing, resuming...', response.data);
3236
3237 // Initialize state from server
3238 self.localIndexingState = {
3239 total: response.data.total,
3240 processed: response.data.processed,
3241 remaining: response.data.remaining,
3242 batchSize: response.data.batch_size,
3243 errors: [],
3244 startTime: Date.now() - ((Date.now() / 1000 - response.data.started_at) * 1000) // Approximate start time
3245 };
3246
3247 // Show progress UI (this also shows stop button and updates status icon)
3248 self.showLocalIndexingProgress();
3249 self.updateLocalIndexingUI(response.data);
3250
3251 // Resume processing
3252 self.processLocalBatches($('.wpforo-ai-reindex-all'));
3253 }
3254 },
3255 error: function() {
3256 // Silent fail - no in-progress indexing
3257 console.log('No in-progress local indexing found');
3258 }
3259 });
3260 },
3261
3262 /**
3263 * Check if returning from purchase and auto-refresh after 60 seconds with countdown
3264 */
3265 checkPostPurchaseRefresh: function() {
3266 // Check if URL has upgraded=1 or credits_purchased=1 parameter
3267 const urlParams = new URLSearchParams(window.location.search);
3268 const isUpgraded = urlParams.get('upgraded') === '1';
3269 const isCreditsPurchased = urlParams.get('credits_purchased') === '1';
3270 const isPostPurchase = isUpgraded || isCreditsPurchased;
3271
3272 if (isPostPurchase) {
3273 console.log('Post-purchase detected, will refresh in 60 seconds...');
3274
3275 // Find existing status badge and update it with countdown
3276 const $statusBadge = $('.wpforo-ai-status-badge').first();
3277 const purchaseType = isUpgraded ? 'Subscription Plan' : 'AI Credits';
3278
3279 if ($statusBadge.length) {
3280 // Update existing badge with countdown message
3281 $statusBadge
3282 .removeClass('status-active status-inactive status-error')
3283 .addClass('status-success')
3284 .html('<span class="dashicons dashicons-update wpforo-status-spin"></span>Updating ' + purchaseType + ' ... (<span class="wpforo-countdown">60</span>s)');
3285
3286 // Start countdown from 60 seconds
3287 let secondsLeft = 60;
3288 const countdownInterval = setInterval(function() {
3289 secondsLeft--;
3290 $statusBadge.find('.wpforo-countdown').text(secondsLeft);
3291
3292 if (secondsLeft <= 0) {
3293 clearInterval(countdownInterval);
3294 // Remove purchase parameters and refresh
3295 window.location.href = window.location.href.split('?')[0] + '?page=wpforo-ai';
3296 }
3297 }, 1000);
3298 } else {
3299 // Fallback: just refresh after 60 seconds if no badge found
3300 setTimeout(function() {
3301 window.location.href = window.location.href.split('?')[0] + '?page=wpforo-ai';
3302 }, 60000);
3303 }
3304 }
3305 }
3306 };
3307
3308 /**
3309 * Initialize when document is ready
3310 */
3311 $(document).ready(function() {
3312 // Initialize on AI Features page or Settings page with bot user search field
3313 if ($('.wpforo-ai-wrap').length || $('#wpforo-ai-bot-user-search').length) {
3314 WpForoAI.init();
3315 }
3316 });
3317
3318 /**
3319 * Make WpForoAI available globally for debugging
3320 */
3321 window.WpForoAI = WpForoAI;
3322
3323 })(jQuery);
3324
3325 /**
3326 * Forum checkbox select all/deselect all with parent-child relationship
3327 */
3328 jQuery(document).ready(function($) {
3329 // Select all forums
3330 $('.wpforo-ai-select-all-forums').on('click', function(e) {
3331 e.preventDefault();
3332 $('.wpforo-ai-forum-checklist input[type="checkbox"]').prop('checked', true);
3333 });
3334
3335 // Deselect all forums
3336 $('.wpforo-ai-deselect-all-forums').on('click', function(e) {
3337 e.preventDefault();
3338 $('.wpforo-ai-forum-checklist input[type="checkbox"]').prop('checked', false);
3339 });
3340
3341 // Parent-child checkbox logic with recursive cascade
3342 $('.wpforo-ai-forum-checklist input[type="checkbox"]').on('change', function() {
3343 const $checkbox = $(this);
3344 const $checklist = $checkbox.closest('.wpforo-ai-forum-checklist');
3345 const forumId = $checkbox.data('forum-id');
3346 const parentId = $checkbox.data('parent-id');
3347 const isChecked = $checkbox.prop('checked');
3348
3349 // Recursive function to check/uncheck all descendants
3350 function setDescendants(fid, checked) {
3351 const $children = $checklist.find('input[data-parent-id="' + fid + '"]');
3352 $children.each(function() {
3353 $(this).prop('checked', checked);
3354 setDescendants($(this).data('forum-id'), checked);
3355 });
3356 }
3357
3358 // Recursive function to uncheck all ancestors
3359 function uncheckAncestors(pid) {
3360 if (pid <= 0) return;
3361 const $parent = $checklist.find('input[data-forum-id="' + pid + '"]');
3362 if ($parent.length) {
3363 $parent.prop('checked', false);
3364 uncheckAncestors($parent.data('parent-id'));
3365 }
3366 }
3367
3368 // Recursive function to check ancestors if all siblings checked
3369 function checkAncestorsIfAllSiblings(pid) {
3370 if (pid <= 0) return;
3371 const $siblings = $checklist.find('input[data-parent-id="' + pid + '"]');
3372 const allChecked = $siblings.length === $siblings.filter(':checked').length;
3373 if (allChecked) {
3374 const $parent = $checklist.find('input[data-forum-id="' + pid + '"]');
3375 $parent.prop('checked', true);
3376 checkAncestorsIfAllSiblings($parent.data('parent-id'));
3377 }
3378 }
3379
3380 // Cascade to all descendants
3381 setDescendants(forumId, isChecked);
3382
3383 // Handle ancestor state
3384 if (!isChecked && parentId > 0) {
3385 uncheckAncestors(parentId);
3386 } else if (isChecked && parentId > 0) {
3387 checkAncestorsIfAllSiblings(parentId);
3388 }
3389 });
3390 });
3391
3392 /**
3393 * AI Tasks Module
3394 * Handles AI task creation, management, and AJAX interactions
3395 */
3396 jQuery(document).ready(function($) {
3397 'use strict';
3398
3399 const WpForoAITasks = {
3400 initialized: false,
3401 editingTaskId: null,
3402 searchTimeout: null,
3403
3404 /**
3405 * Initialize AI Tasks functionality
3406 */
3407 init: function() {
3408 if (this.initialized) {
3409 return;
3410 }
3411 this.initialized = true;
3412 this.bindEvents();
3413 },
3414
3415 /**
3416 * Bind event handlers
3417 */
3418 bindEvents: function() {
3419 const self = this;
3420
3421 // Unbind all task events first to prevent duplicates
3422 $(document).off('click', '.wpforo-ai-create-task-btn');
3423 $(document).off('click', '.wpforo-ai-cancel-task-btn');
3424 $(document).off('change', '#wpforo-ai-task-type');
3425 $(document).off('click', '#wpforo-ai-save-task-btn');
3426 $(document).off('submit', '#wpforo-ai-task-form');
3427 $(document).off('click', '.wpforo-ai-task-actions-toggle');
3428 $(document).off('click', '.wpforo-ai-task-run');
3429 $(document).off('click', '.wpforo-ai-task-pause');
3430 $(document).off('click', '.wpforo-ai-task-activate');
3431 $(document).off('click', '.wpforo-ai-task-edit');
3432 $(document).off('click', '.wpforo-ai-task-delete');
3433 $(document).off('click', '.wpforo-ai-task-duplicate');
3434 $(document).off('click', '.wpforo-ai-task-stats');
3435 $(document).off('click', '.wpforo-ai-task-logs');
3436 $(document).off('click', '.wpforo-ai-bulk-apply');
3437 $(document).off('change', '.wpforo-ai-select-all-tasks');
3438 $(document).off('change', '.wpforo-ai-filter-status, .wpforo-ai-filter-type');
3439 $(document).off('keyup', '.wpforo-ai-search-tasks');
3440
3441 // Actions dropdown toggle
3442 $(document).on('click', '.wpforo-ai-task-actions-toggle', function(e) {
3443 e.preventDefault();
3444 e.stopPropagation();
3445 const $dropdown = $(this).closest('.wpforo-ai-task-actions-dropdown');
3446 const isOpen = $dropdown.hasClass('open');
3447
3448 // Close all other dropdowns first
3449 $('.wpforo-ai-task-actions-dropdown').removeClass('open');
3450
3451 // Toggle current dropdown
3452 if (!isOpen) {
3453 $dropdown.addClass('open');
3454 }
3455 });
3456
3457 // Close dropdown when clicking outside
3458 $(document).on('click', function(e) {
3459 if (!$(e.target).closest('.wpforo-ai-task-actions-dropdown').length) {
3460 $('.wpforo-ai-task-actions-dropdown').removeClass('open');
3461 }
3462 });
3463
3464 // Close dropdown when clicking a menu item
3465 $(document).on('click', '.wpforo-ai-task-actions-menu a', function() {
3466 $(this).closest('.wpforo-ai-task-actions-dropdown').removeClass('open');
3467 });
3468
3469 // Create Task button - toggle form visibility (with debounce)
3470 let isToggling = false;
3471 $(document).on('click', '.wpforo-ai-create-task-btn', function(e) {
3472 e.preventDefault();
3473 e.stopPropagation();
3474 if (isToggling) {
3475 console.log('Debounced - toggle already in progress');
3476 return;
3477 }
3478 isToggling = true;
3479 self.toggleTaskForm();
3480 setTimeout(function() { isToggling = false; }, 500);
3481 });
3482
3483 // Cancel button - hide form
3484 $(document).on('click', '.wpforo-ai-cancel-task-btn', function(e) {
3485 e.preventDefault();
3486 e.stopPropagation();
3487 self.hideTaskForm();
3488 });
3489
3490 // Task type selection - show dynamic config
3491 $(document).on('change', '#wpforo-ai-task-type', function() {
3492 self.handleTaskTypeChange($(this).val());
3493 });
3494
3495 // Day checkbox toggle styling
3496 $(document).on('change', '.wpforo-ai-day-checkboxes input', function() {
3497 const $label = $(this).closest('label');
3498 if ($(this).is(':checked')) {
3499 $label.addClass('selected');
3500 } else {
3501 $label.removeClass('selected');
3502 }
3503 });
3504
3505 // Quality tier selection
3506 $(document).on('click', '.wpforo-ai-quality-tier', function() {
3507 const $tier = $(this);
3508 const $input = $tier.find('input[type="radio"]');
3509
3510 // Remove selection from all tiers in this group
3511 $tier.siblings('.wpforo-ai-quality-tier').removeClass('selected');
3512 $tier.addClass('selected');
3513 $input.prop('checked', true);
3514 });
3515
3516 // Duplicate prevention checkbox toggle
3517 $(document).on('change', '[name="config[duplicate_prevention]"]', function() {
3518 const $checkbox = $(this);
3519 const $section = $checkbox.closest('.wpforo-ai-column');
3520 const $duplicateSettings = $section.find('.wpforo-ai-duplicate-settings');
3521
3522 if ($checkbox.is(':checked')) {
3523 $duplicateSettings.slideDown(200);
3524 } else {
3525 $duplicateSettings.slideUp(200);
3526 }
3527 });
3528
3529 // Run on approval toggle - hide/disable scheduled options
3530 $(document).on('change', '.wpforo-ai-run-on-approval-checkbox', function() {
3531 const $checkbox = $(this);
3532 // Look for scheduled options in either column or form-section (Tag Generator uses form-section)
3533 let $section = $checkbox.closest('.wpforo-ai-column');
3534 if (!$section.length) {
3535 $section = $checkbox.closest('.wpforo-ai-form-section');
3536 }
3537 const $scheduledOptions = $section.find('.wpforo-ai-scheduled-options');
3538
3539 if ($checkbox.is(':checked')) {
3540 $scheduledOptions.slideUp(200);
3541 // Disable inputs to prevent form validation errors
3542 $scheduledOptions.find('input, select').prop('disabled', true);
3543 } else {
3544 $scheduledOptions.slideDown(200);
3545 $scheduledOptions.find('input, select').prop('disabled', false);
3546 }
3547
3548 // Update estimated credits (will be different for on-approval mode)
3549 self.updateEstimatedCredits();
3550 });
3551
3552 // Author mutual exclusion: usergroup selected → clear user field
3553 $(document).on('change', '.wpforo-ai-author-groupid-select', function() {
3554 if ($(this).val()) {
3555 const $section = $(this).closest('.wpforo-ai-form-section');
3556 $section.find('.wpforo-ai-user-id-input').val('');
3557 $section.find('.wpforo-ai-user-search').val('');
3558 }
3559 });
3560
3561 // Credit estimation - update on field changes
3562 $(document).on('change', '[name="config[frequency]"], [name="config[topics_per_run]"], [name="config[replies_per_run]"], [name="config[quality_tier]"], [name="config[active_days][]"]', function() {
3563 self.updateEstimatedCredits();
3564 });
3565
3566 // Save Task button (by ID) and form submit
3567 $(document).on('click', '#wpforo-ai-save-task-btn', function(e) {
3568 e.preventDefault();
3569 self.saveTask();
3570 });
3571
3572 // Also handle form submit to prevent default
3573 $(document).on('submit', '#wpforo-ai-task-form', function(e) {
3574 e.preventDefault();
3575 self.saveTask();
3576 });
3577
3578 // Task actions - Run
3579 $(document).on('click', '.wpforo-ai-task-run', function(e) {
3580 e.preventDefault();
3581 const taskId = $(this).closest('tr').data('task-id');
3582 self.runTask(taskId);
3583 });
3584
3585 // Task actions - Pause
3586 $(document).on('click', '.wpforo-ai-task-pause', function(e) {
3587 e.preventDefault();
3588 const taskId = $(this).closest('tr').data('task-id');
3589 self.toggleTaskStatus(taskId, 'paused');
3590 });
3591
3592 // Task actions - Activate
3593 $(document).on('click', '.wpforo-ai-task-activate', function(e) {
3594 e.preventDefault();
3595 const taskId = $(this).closest('tr').data('task-id');
3596 self.toggleTaskStatus(taskId, 'active');
3597 });
3598
3599 // Task actions - Duplicate
3600 $(document).on('click', '.wpforo-ai-task-duplicate', function(e) {
3601 e.preventDefault();
3602 const taskId = $(this).closest('tr').data('task-id');
3603 self.duplicateTask(taskId);
3604 });
3605
3606 // Task actions - View Stats
3607 $(document).on('click', '.wpforo-ai-task-stats', function(e) {
3608 e.preventDefault();
3609 const taskId = $(this).closest('tr').data('task-id');
3610 self.viewTaskStats(taskId);
3611 });
3612
3613 // Task actions - Edit
3614 $(document).on('click', '.wpforo-ai-task-edit', function(e) {
3615 e.preventDefault();
3616 const taskId = $(this).closest('tr').data('task-id');
3617 self.editTask(taskId);
3618 });
3619
3620 // Task actions - Delete
3621 $(document).on('click', '.wpforo-ai-task-delete', function(e) {
3622 e.preventDefault();
3623 const taskId = $(this).closest('tr').data('task-id');
3624 self.deleteTask(taskId);
3625 });
3626
3627 // Task actions - View Logs
3628 $(document).on('click', '.wpforo-ai-task-logs', function(e) {
3629 e.preventDefault();
3630 const taskId = $(this).closest('tr').data('task-id');
3631 self.viewTaskLogs(taskId);
3632 });
3633
3634 // Bulk actions
3635 $(document).on('click', '.wpforo-ai-bulk-apply', function(e) {
3636 e.preventDefault();
3637 self.applyBulkAction();
3638 });
3639
3640 // Select all checkbox
3641 $(document).on('change', '.wpforo-ai-select-all-tasks', function() {
3642 $('.wpforo-ai-task-checkbox').prop('checked', $(this).is(':checked'));
3643 });
3644
3645 // Filter change
3646 $(document).on('change', '.wpforo-ai-filter-status, .wpforo-ai-filter-type', function() {
3647 self.filterTasks();
3648 });
3649
3650 // Search
3651 $(document).on('keyup', '.wpforo-ai-search-tasks', function() {
3652 clearTimeout(self.searchTimeout);
3653 self.searchTimeout = setTimeout(function() {
3654 self.filterTasks();
3655 }, 300);
3656 });
3657 },
3658
3659 /**
3660 * Toggle task form visibility
3661 */
3662 toggleTaskForm: function() {
3663 const $container = $('.wpforo-ai-task-form-container');
3664 const $btn = $('.wpforo-ai-create-task-btn');
3665
3666 if ($container.hasClass('visible')) {
3667 this.hideTaskForm();
3668 } else {
3669 // Show the form with inline styles to ensure visibility
3670 $container.addClass('visible').css({
3671 'display': 'block',
3672 'visibility': 'visible',
3673 'opacity': '1'
3674 });
3675 $btn.html('<span class="dashicons dashicons-no-alt"></span> Cancel');
3676
3677 // Reset form if not editing
3678 if (!this.editingTaskId) {
3679 this.resetForm();
3680 }
3681
3682 // Scroll to form using native scrollIntoView for better compatibility
3683 $container[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
3684 }
3685 },
3686
3687 /**
3688 * Hide task form
3689 */
3690 hideTaskForm: function() {
3691 const $container = $('.wpforo-ai-task-form-container');
3692 const $btn = $('.wpforo-ai-create-task-btn');
3693
3694 $container.removeClass('visible').css({
3695 'display': 'none',
3696 'visibility': '',
3697 'opacity': ''
3698 });
3699 $btn.html('<span class="dashicons dashicons-plus-alt2"></span> Create AI Task');
3700
3701 // Reset editing state
3702 this.editingTaskId = null;
3703 this.resetForm();
3704 },
3705
3706 /**
3707 * Reset form to defaults
3708 */
3709 resetForm: function() {
3710 const $form = $('#wpforo-ai-task-form');
3711 if ($form.length) {
3712 $form[0].reset();
3713 }
3714
3715 // Clear config section completely to prevent cached checkbox values
3716 $('#wpforo-ai-task-config-section').empty().hide();
3717
3718 // Reset task type select
3719 $('#wpforo-ai-task-type').val('');
3720
3721 // Hide dynamic config sections
3722 $('.wpforo-ai-dynamic-config').removeClass('visible');
3723
3724 // Reset day checkboxes styling
3725 $('.wpforo-ai-day-checkboxes label').removeClass('selected');
3726
3727 // Reset quality tier selection
3728 $('.wpforo-ai-quality-tier').removeClass('selected');
3729
3730 // Explicitly uncheck all forum checkboxes (in case of browser caching)
3731 $('.forum-checkbox').prop('checked', false);
3732
3733 // Update form header
3734 $('.wpforo-ai-task-form-box .wpforo-ai-box-header h2').html(
3735 '<span class="dashicons dashicons-plus-alt2"></span> Create New AI Task'
3736 );
3737 },
3738
3739 /**
3740 * Calculate and update estimated monthly credits
3741 * Based on: frequency × items_per_run × credits_per_tier × active_days_factor
3742 */
3743 updateEstimatedCredits: function() {
3744 const taskType = $('#wpforo-ai-task-type').val();
3745 if (!taskType) return;
3746
3747 const $configSection = $('#wpforo-ai-task-config-section');
3748 const $estimatedValue = $configSection.find('.wpforo-ai-estimated-credits-value');
3749 if (!$estimatedValue.length) return;
3750
3751 // Get frequency
3752 const frequency = $configSection.find('[name="config[frequency]"]').val() || 'daily';
3753
3754 // Get items per run based on task type
3755 let itemsPerRun = 1;
3756 if (taskType === 'topic_generator') {
3757 itemsPerRun = parseInt($configSection.find('[name="config[topics_per_run]"]').val()) || 1;
3758 } else if (taskType === 'reply_generator') {
3759 itemsPerRun = parseInt($configSection.find('[name="config[replies_per_run]"]').val()) || 1;
3760 }
3761
3762 // Get quality tier credits
3763 const qualityTier = $configSection.find('[name="config[quality_tier]"]').val() || 'balanced';
3764 const creditsPerItem = {
3765 'fast': 1,
3766 'balanced': 2,
3767 'advanced': 3,
3768 'premium': 4
3769 }[qualityTier] || 2;
3770
3771 // Get active days count (default all 7)
3772 const activeDays = $configSection.find('[name="config[active_days][]"]:checked').length || 7;
3773 const activeDaysFactor = activeDays / 7;
3774
3775 // Calculate runs per month based on frequency
3776 const runsPerMonth = {
3777 'hourly': 24 * 30, // 720
3778 '2hours': 12 * 30, // 360
3779 '3hours': 8 * 30, // 240
3780 '4hours': 6 * 30, // 180
3781 '6hours': 4 * 30, // 120
3782 '12hours': 2 * 30, // 60
3783 'daily': 30, // 30
3784 '3days': 10, // 10 (30/3)
3785 'weekly': 4, // 4
3786 'monthly': 1 // 1
3787 }[frequency] || 30;
3788
3789 // Calculate estimated monthly credits
3790 const estimatedCredits = Math.round(runsPerMonth * itemsPerRun * creditsPerItem * activeDaysFactor);
3791
3792 // Format with comma for thousands
3793 const formattedCredits = estimatedCredits.toLocaleString();
3794
3795 // Update display
3796 $estimatedValue.text('~' + formattedCredits + ' credits');
3797
3798 // Calculate and update manual run cost (itemsPerRun × creditsPerItem)
3799 const manualRunCost = itemsPerRun * creditsPerItem;
3800 const $manualRunCostValue = $configSection.find('.wpforo-ai-manual-run-cost-value');
3801 if ($manualRunCostValue.length) {
3802 $manualRunCostValue.text(manualRunCost + ' credit' + (manualRunCost !== 1 ? 's' : ''));
3803 }
3804 },
3805
3806 /**
3807 * Handle task type selection change
3808 */
3809 handleTaskTypeChange: function(taskType) {
3810 const $configSection = $('#wpforo-ai-task-config-section');
3811
3812 // Hide language dropdown for tag maintenance (tags match topic content language)
3813 const $languageField = $('#wpforo-ai-task-language').closest('.wpforo-ai-form-field');
3814 if (taskType === 'tag_maintenance') {
3815 $languageField.hide();
3816 } else {
3817 $languageField.show();
3818 }
3819
3820 // Clear and hide if no type selected
3821 if (!taskType) {
3822 $configSection.empty().hide();
3823 return;
3824 }
3825
3826 // Get template content from script tag
3827 const $template = $('#wpforo-ai-task-config-' + taskType);
3828 if ($template.length) {
3829 // Load template content into config section
3830 $configSection.html($template.html()).show();
3831
3832 // Initialize dynamic form elements after loading template
3833 this.initDynamicFormElements($configSection);
3834
3835 // Update estimated credits for the new task type
3836 this.updateEstimatedCredits();
3837 } else {
3838 console.error('Template not found for task type:', taskType);
3839 $configSection.empty().hide();
3840 }
3841 },
3842
3843 /**
3844 * Initialize dynamic form elements (collapsible sections, range sliders)
3845 */
3846 initDynamicFormElements: function($container) {
3847 // Initialize collapsible sections
3848 $container.find('.wpforo-ai-collapsible-toggle').each(function() {
3849 const $toggle = $(this);
3850 const $content = $toggle.next('.wpforo-ai-collapsible-content');
3851
3852 // Set initial state
3853 const isExpanded = $toggle.attr('aria-expanded') === 'true';
3854 if (!isExpanded) {
3855 $content.hide();
3856 }
3857
3858 // Remove any existing click handlers and add new one
3859 $toggle.off('click').on('click', function(e) {
3860 e.preventDefault();
3861 const currentlyExpanded = $toggle.attr('aria-expanded') === 'true';
3862
3863 if (currentlyExpanded) {
3864 $toggle.attr('aria-expanded', 'false');
3865 $content.slideUp(300);
3866 } else {
3867 $toggle.attr('aria-expanded', 'true');
3868 $content.slideDown(300);
3869 }
3870 });
3871 });
3872
3873 // Initialize range sliders
3874 $container.find('.wpforo-ai-range-slider').each(function() {
3875 const $slider = $(this);
3876 const $valueDisplay = $slider.next('.wpforo-ai-range-value');
3877
3878 // Set initial value display
3879 if ($valueDisplay.length) {
3880 $valueDisplay.text($slider.val() + '%');
3881 }
3882
3883 // Update value on input
3884 $slider.off('input').on('input', function() {
3885 if ($valueDisplay.length) {
3886 $valueDisplay.text($(this).val() + '%');
3887 }
3888 });
3889 });
3890
3891 // Initialize forum select all/deselect all buttons within container
3892 $container.find('.wpforo-ai-select-all-forums').off('click').on('click', function(e) {
3893 e.preventDefault();
3894 $(this).closest('.wpforo-ai-form-field').find('.wpforo-ai-forum-checkbox-item input[type="checkbox"]').prop('checked', true);
3895 });
3896
3897 $container.find('.wpforo-ai-deselect-all-forums').off('click').on('click', function(e) {
3898 e.preventDefault();
3899 $(this).closest('.wpforo-ai-form-field').find('.wpforo-ai-forum-checkbox-item input[type="checkbox"]').prop('checked', false);
3900 });
3901
3902 // Initialize parent/category checkbox toggle behavior with recursive cascade
3903 $container.find('.forum-parent-toggle').off('change').on('change', function() {
3904 const $parent = $(this);
3905 const parentId = $parent.data('forum-id');
3906 const isChecked = $parent.prop('checked');
3907 const $checklist = $parent.closest('.wpforo-ai-forum-checklist');
3908
3909 // Recursive function to set all descendants
3910 function setDescendants(fid, checked) {
3911 $checklist.find('.forum-checkbox[data-parent-id="' + fid + '"]').each(function() {
3912 $(this).prop('checked', checked);
3913 setDescendants($(this).data('forum-id'), checked);
3914 });
3915 }
3916
3917 setDescendants(parentId, isChecked);
3918 });
3919
3920 // Update parent checkbox state when child checkboxes change
3921 $container.find('.forum-checkbox:not(.forum-parent-toggle)').off('change').on('change', function() {
3922 const $child = $(this);
3923 const $checklist = $child.closest('.wpforo-ai-forum-checklist');
3924 const forumId = $child.data('forum-id');
3925 const parentId = $child.data('parent-id');
3926 const isChecked = $child.prop('checked');
3927
3928 // Cascade to descendants
3929 function setDescendants(fid, checked) {
3930 $checklist.find('.forum-checkbox[data-parent-id="' + fid + '"]').each(function() {
3931 $(this).prop('checked', checked);
3932 setDescendants($(this).data('forum-id'), checked);
3933 });
3934 }
3935 setDescendants(forumId, isChecked);
3936
3937 // Update ancestor states
3938 function updateAncestors(pid) {
3939 if (!pid) return;
3940 const $parent = $checklist.find('.forum-checkbox[data-forum-id="' + pid + '"]');
3941 if (!$parent.length) return;
3942
3943 const $siblings = $checklist.find('.forum-checkbox[data-parent-id="' + pid + '"]');
3944 const allChecked = $siblings.length > 0 && $siblings.filter(':checked').length === $siblings.length;
3945 const someChecked = $siblings.filter(':checked').length > 0;
3946
3947 $parent.prop('checked', allChecked);
3948 $parent.prop('indeterminate', someChecked && !allChecked);
3949 updateAncestors($parent.data('parent-id'));
3950 }
3951 updateAncestors(parentId);
3952 });
3953
3954 // Initialize duplicate prevention toggle state
3955 $container.find('[name="config[duplicate_prevention]"]').each(function() {
3956 const $checkbox = $(this);
3957 const $section = $checkbox.closest('.wpforo-ai-column');
3958 const $duplicateSettings = $section.find('.wpforo-ai-duplicate-settings');
3959
3960 // Set initial visibility based on checkbox state
3961 if ($checkbox.is(':checked')) {
3962 $duplicateSettings.show();
3963 } else {
3964 $duplicateSettings.hide();
3965 }
3966 });
3967
3968 // Initialize user search fields
3969 this.initUserSearch($container);
3970 },
3971
3972 /**
3973 * Initialize AJAX user search for author selection
3974 */
3975 initUserSearch: function($container) {
3976 const self = this;
3977 let searchTimeout = null;
3978
3979 $container.find('.wpforo-ai-user-search').each(function() {
3980 const $searchInput = $(this);
3981 const $wrapper = $searchInput.closest('.wpforo-ai-user-search-wrapper');
3982 const $hiddenInput = $wrapper.find('.wpforo-ai-user-id-input');
3983 const $resultsContainer = $wrapper.find('.wpforo-ai-user-search-results');
3984
3985 // Handle input for search
3986 $searchInput.off('input').on('input', function() {
3987 const searchTerm = $(this).val().trim();
3988
3989 // Clear previous timeout
3990 if (searchTimeout) {
3991 clearTimeout(searchTimeout);
3992 }
3993
3994 // Clear results if search term is too short
3995 if (searchTerm.length < 2) {
3996 $resultsContainer.empty().hide();
3997 return;
3998 }
3999
4000 // Debounce the search
4001 searchTimeout = setTimeout(function() {
4002 self.searchUsers(searchTerm, $resultsContainer, $hiddenInput, $searchInput);
4003 }, 300);
4004 });
4005
4006 // Handle click outside to close results
4007 $(document).on('click', function(e) {
4008 if (!$(e.target).closest('.wpforo-ai-user-search-wrapper').length) {
4009 $resultsContainer.empty().hide();
4010 }
4011 });
4012
4013 // Handle focus to show results if there's a search term
4014 $searchInput.off('focus').on('focus', function() {
4015 if ($(this).val().trim().length >= 2 && $resultsContainer.children().length > 0) {
4016 $resultsContainer.show();
4017 }
4018 });
4019 });
4020 },
4021
4022 /**
4023 * Perform AJAX user search
4024 */
4025 searchUsers: function(searchTerm, $resultsContainer, $hiddenInput, $searchInput) {
4026 $resultsContainer.html('<div class="wpforo-ai-user-search-loading">Searching...</div>').show();
4027
4028 // Get AJAX URL and nonce from localized script and hidden input
4029 const ajaxUrl = (typeof wpforoAIAdmin !== 'undefined' && wpforoAIAdmin.ajaxUrl) ? wpforoAIAdmin.ajaxUrl : ajaxurl;
4030 const nonce = $('#wpforo-ai-task-nonce').val() || '';
4031
4032 $.ajax({
4033 url: ajaxUrl,
4034 type: 'POST',
4035 data: {
4036 action: 'wpforo_ai_search_users',
4037 search: searchTerm,
4038 _wpnonce: nonce
4039 },
4040 success: function(response) {
4041 $resultsContainer.empty();
4042
4043 if (response.success && response.data.users && response.data.users.length > 0) {
4044 const $list = $('<ul class="wpforo-ai-user-search-list"></ul>');
4045
4046 response.data.users.forEach(function(user) {
4047 const $item = $('<li class="wpforo-ai-user-search-item" data-user-id="' + user.id + '"></li>');
4048 $item.text(user.label);
4049 $item.on('click', function() {
4050 $hiddenInput.val(user.id);
4051 $searchInput.val(user.label);
4052 $resultsContainer.empty().hide();
4053 // Clear usergroup when specific user is selected
4054 $hiddenInput.closest('.wpforo-ai-form-section').find('.wpforo-ai-author-groupid-select').val('');
4055 });
4056 $list.append($item);
4057 });
4058
4059 $resultsContainer.append($list).show();
4060 } else {
4061 $resultsContainer.html('<div class="wpforo-ai-user-search-empty">No users found</div>').show();
4062 }
4063 },
4064 error: function() {
4065 $resultsContainer.html('<div class="wpforo-ai-user-search-error">Search error</div>').show();
4066 }
4067 });
4068 },
4069
4070 /**
4071 * Collect form data
4072 */
4073 collectFormData: function() {
4074 const taskType = $('#wpforo-ai-task-type').val();
4075
4076 // Basic task data
4077 const data = {
4078 task_name: $('#wpforo-ai-task-name').val(),
4079 task_type: taskType,
4080 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4081 status: $('input[name="status"]:checked').val() || 'paused'
4082 };
4083
4084 // Collect type-specific config
4085 const config = {};
4086
4087 // Language setting (shared across all task types)
4088 config.response_language = $('#wpforo-ai-task-language').val() || '';
4089
4090 // Config section where dynamic form is rendered
4091 const $configSection = $('#wpforo-ai-task-config-section');
4092
4093 if (taskType === 'topic_generator') {
4094 // Get checked forum IDs (use correct name attribute from form)
4095 config.target_forums = this.getCheckedValues($configSection.find('[name="config[target_forum_ids][]"]'));
4096
4097 // Content settings
4098 config.topic_theme = $configSection.find('[name="config[topic_theme]"]').val() || '';
4099 config.topic_style = $configSection.find('[name="config[topic_style]"]').val() || 'neutral';
4100 config.topic_tone = $configSection.find('[name="config[topic_tone]"]').val() || 'neutral';
4101 config.content_length = $configSection.find('[name="config[content_length]"]').val() || 'medium';
4102
4103 // Content options (what to include)
4104 config.include_code = $configSection.find('[name="config[include_code]"]').is(':checked');
4105 config.include_links = $configSection.find('[name="config[include_links]"]').is(':checked');
4106 config.include_steps = $configSection.find('[name="config[include_steps]"]').is(':checked');
4107 config.include_youtube = $configSection.find('[name="config[include_youtube]"]').is(':checked');
4108
4109 // Author settings
4110 config.author_userid = parseInt($configSection.find('[name="config[author_userid]"]').val()) || 0;
4111 config.author_groupid = parseInt($configSection.find('[name="config[author_groupid]"]').val()) || 0;
4112 config.show_ai_badge = $configSection.find('[name="config[show_ai_badge]"]').is(':checked');
4113
4114 // Scheduling
4115 config.frequency = $configSection.find('[name="config[frequency]"]').val() || 'daily';
4116 config.topics_per_run = parseInt($configSection.find('[name="config[topics_per_run]"]').val()) || 1;
4117 config.active_days = this.getCheckedValues($configSection.find('[name="config[active_days][]"]:checked'));
4118
4119 // AI Quality & Credits
4120 config.quality_tier = $configSection.find('[name="config[quality_tier]"]').val() || 'balanced';
4121 config.credit_stop_threshold = parseInt($configSection.find('[name="config[credit_stop_threshold]"]').val()) || 0;
4122 config.auto_pause_on_limit = $configSection.find('[name="config[auto_pause_on_limit]"]').is(':checked');
4123
4124 // Content Safety
4125 config.duplicate_prevention = $configSection.find('[name="config[duplicate_prevention]"]').is(':checked');
4126 config.similarity_threshold = parseInt($configSection.find('[name="config[similarity_threshold]"]').val()) || 75;
4127 config.duplicate_check_days = parseInt($configSection.find('[name="config[duplicate_check_days]"]').val()) || 90;
4128 config.topic_status = parseInt($configSection.find('[name="config[topic_status]"]:checked').val()) || 0;
4129
4130 // Advanced Options
4131 config.topic_prefix = $configSection.find('[name="config[topic_prefix]"]').val() || '';
4132 config.topic_prefix_id = $configSection.find('[name="config[topic_prefix_id]"]').val() || '';
4133 config.auto_tags = $configSection.find('[name="config[auto_tags]"]').val() || '';
4134 config.search_keywords = $configSection.find('[name="config[search_keywords]"]').val() || '';
4135 } else if (taskType === 'reply_generator') {
4136 // Target settings - forums, topic IDs, and date range
4137 config.reply_target_forums = this.getCheckedValues($configSection.find('[name="config[reply_target_forum_ids][]"]'));
4138 config.target_topic_ids = $configSection.find('[name="config[target_topic_ids]"]').val() || '';
4139 config.only_not_replied = $configSection.find('[name="config[only_not_replied]"]').is(':checked');
4140 config.date_range_from = $configSection.find('[name="config[date_range_from]"]').val() || '';
4141 config.date_range_to = $configSection.find('[name="config[date_range_to]"]').val() || '';
4142 config.reply_style = $configSection.find('[name="config[reply_style]"]').val() || 'neutral';
4143 config.reply_tone = $configSection.find('[name="config[reply_tone]"]').val() || 'neutral';
4144 config.response_guidelines = $configSection.find('[name="config[response_guidelines]"]').val() || '';
4145 config.reply_length = $configSection.find('[name="config[reply_length]"]').val() || 'medium';
4146 config.knowledge_source = $configSection.find('[name="config[knowledge_source]"]').val() || 'forum_only';
4147 config.no_content_action = $configSection.find('[name="config[no_content_action]"]').val() || 'use_ai_fallback';
4148 config.author_userid = $configSection.find('[name="config[author_userid]"]').val() || 0;
4149 config.author_groupid = parseInt($configSection.find('[name="config[author_groupid]"]').val()) || 0;
4150 config.show_ai_badge = $configSection.find('[name="config[show_ai_badge]"]').is(':checked');
4151 // Scheduling - Run on approval OR scheduled
4152 config.run_on_approval = $configSection.find('[name="config[run_on_approval]"]').is(':checked');
4153 config.frequency = $configSection.find('[name="config[frequency]"]').val() || '3hours';
4154 config.replies_per_run = parseInt($configSection.find('[name="config[replies_per_run]"]').val()) || 3;
4155 config.active_days = this.getCheckedValues($configSection.find('[name="config[active_days][]"]:checked'));
4156 config.quality_tier = $configSection.find('[name="config[quality_tier]"]').val() || 'balanced';
4157 config.credit_stop_threshold = parseInt($configSection.find('[name="config[credit_stop_threshold]"]').val()) || 0;
4158 config.auto_pause_on_limit = $configSection.find('[name="config[auto_pause_on_limit]"]').is(':checked');
4159 config.duplicate_prevention = $configSection.find('[name="config[duplicate_prevention]"]').is(':checked');
4160 config.similarity_threshold = parseInt($configSection.find('[name="config[similarity_threshold]"]').val()) || 75;
4161 config.duplicate_check_days = parseInt($configSection.find('[name="config[duplicate_check_days]"]').val()) || 90;
4162 config.reply_status = parseInt($configSection.find('[name="config[reply_status]"]:checked').val()) || 0;
4163 config.reply_strategy = $configSection.find('[name="config[reply_strategy]"]').val() || 'first_post';
4164 // Reply content options
4165 config.reply_include_code = $configSection.find('[name="config[reply_include_code]"]').is(':checked');
4166 config.reply_include_links = $configSection.find('[name="config[reply_include_links]"]').is(':checked');
4167 config.reply_include_steps = $configSection.find('[name="config[reply_include_steps]"]').is(':checked');
4168 config.reply_include_followup = $configSection.find('[name="config[reply_include_followup]"]').is(':checked');
4169 config.reply_include_youtube = $configSection.find('[name="config[reply_include_youtube]"]').is(':checked');
4170 config.reply_include_greeting = $configSection.find('[name="config[reply_include_greeting]"]').is(':checked');
4171 config.max_replies_per_topic = parseInt($configSection.find('[name="config[max_replies_per_topic]"]').val()) || 1;
4172 } else if (taskType === 'tag_maintenance') {
4173 // Target settings
4174 config.tag_target_forum_ids = this.getCheckedValues($configSection.find('[name="config[tag_target_forum_ids][]"]'));
4175 config.target_topic_ids = $configSection.find('[name="config[target_topic_ids]"]').val() || '';
4176 config.date_range_from = $configSection.find('[name="config[date_range_from]"]').val() || '';
4177 config.date_range_to = $configSection.find('[name="config[date_range_to]"]').val() || '';
4178 config.only_not_tagged = $configSection.find('[name="config[only_not_tagged]"]').is(':checked');
4179
4180 // Tag options
4181 config.max_tags = parseInt($configSection.find('[name="config[max_tags]"]').val()) || 5;
4182 config.preserve_existing = $configSection.find('[name="config[preserve_existing]"]').is(':checked');
4183 config.maintain_vocabulary = $configSection.find('[name="config[maintain_vocabulary]"]').is(':checked');
4184 config.remove_duplicates = $configSection.find('[name="config[remove_duplicates]"]').is(':checked');
4185 config.remove_irrelevant = $configSection.find('[name="config[remove_irrelevant]"]').is(':checked');
4186 config.lowercase = $configSection.find('[name="config[lowercase]"]').is(':checked');
4187
4188 // Scheduling - Run on approval OR scheduled
4189 config.run_on_approval = $configSection.find('[name="config[run_on_approval]"]').is(':checked');
4190 config.frequency = $configSection.find('[name="config[frequency]"]').val() || 'daily';
4191 config.topics_per_run = parseInt($configSection.find('[name="config[topics_per_run]"]').val()) || 20;
4192 config.active_days = this.getCheckedValues($configSection.find('[name="config[active_days][]"]:checked'));
4193
4194 // AI Quality & Credits
4195 config.quality_tier = $configSection.find('[name="config[quality_tier]"]').val() || 'premium';
4196 config.credit_stop_threshold = parseInt($configSection.find('[name="config[credit_stop_threshold]"]').val()) || 0;
4197 config.auto_pause_on_limit = $configSection.find('[name="config[auto_pause_on_limit]"]').is(':checked');
4198 }
4199
4200 data.config = JSON.stringify(config);
4201
4202 return data;
4203 },
4204
4205 /**
4206 * Get checked checkbox values
4207 * @param {string|jQuery} selectorOrElements - CSS selector string or jQuery object
4208 */
4209 getCheckedValues: function(selectorOrElements) {
4210 const values = [];
4211 // Handle both string selectors and jQuery objects
4212 let $elements;
4213 if (typeof selectorOrElements === 'string') {
4214 $elements = $(selectorOrElements + ':checked');
4215 } else {
4216 // Already jQuery object, filter for checked if not already
4217 $elements = selectorOrElements.filter(':checked').length ?
4218 selectorOrElements.filter(':checked') : selectorOrElements;
4219 }
4220 $elements.each(function() {
4221 const val = $(this).val();
4222 if (val) {
4223 values.push(val);
4224 }
4225 });
4226 return values;
4227 },
4228
4229 /**
4230 * Validate form
4231 */
4232 validateForm: function() {
4233 const taskName = $('#wpforo-ai-task-name').val().trim();
4234 const taskType = $('#wpforo-ai-task-type').val();
4235 const $configSection = $('#wpforo-ai-task-config-section');
4236
4237 if (!taskName) {
4238 alert('Please enter a task name.');
4239 $('#wpforo-ai-task-name').focus();
4240 return false;
4241 }
4242
4243 if (!taskType) {
4244 alert('Please select a task type.');
4245 $('#wpforo-ai-task-type').focus();
4246 return false;
4247 }
4248
4249 // Author validation for topic and reply generators
4250 if (taskType === 'topic_generator' || taskType === 'reply_generator') {
4251 const authorUserId = parseInt($configSection.find('[name="config[author_userid]"]').val()) || 0;
4252 const authorGroupId = parseInt($configSection.find('[name="config[author_groupid]"]').val()) || 0;
4253 if (!authorUserId && !authorGroupId) {
4254 alert('Please select either an author user or an author usergroup.');
4255 return false;
4256 }
4257 }
4258
4259 // Type-specific validation
4260 if (taskType === 'topic_generator') {
4261 // Validate at least one forum is selected
4262 const selectedForums = $configSection.find('[name="config[target_forum_ids][]"]:checked').length;
4263 if (selectedForums === 0) {
4264 alert('Please select at least one target forum.');
4265 return false;
4266 }
4267 } else if (taskType === 'reply_generator') {
4268 // Validate at least one target is specified (forums OR date range OR topic IDs)
4269 const selectedForums = $configSection.find('[name="config[reply_target_forum_ids][]"]:checked').length;
4270 const topicIds = $configSection.find('[name="config[target_topic_ids]"]').val().trim();
4271 const dateFrom = $configSection.find('[name="config[date_range_from]"]').val();
4272 const dateTo = $configSection.find('[name="config[date_range_to]"]').val();
4273 const hasDateRange = dateFrom || dateTo;
4274
4275 if (selectedForums === 0 && !topicIds && !hasDateRange) {
4276 alert('Please select at least one target: forums, date range, or specific topic IDs.');
4277 return false;
4278 }
4279 }
4280
4281 return true;
4282 },
4283
4284 /**
4285 * Save task via AJAX
4286 */
4287 saveTask: function() {
4288 if (!this.validateForm()) {
4289 return;
4290 }
4291
4292 const $saveBtn = $('#wpforo-ai-save-task-btn');
4293 const originalText = $saveBtn.html();
4294
4295 // Disable button and show loading
4296 $saveBtn.prop('disabled', true).html('<span class="dashicons dashicons-update wpforo-save-spin"></span> Saving...');
4297
4298 const formData = this.collectFormData();
4299 formData.action = 'wpforo_ai_save_task';
4300 formData._wpnonce = $('#wpforo-ai-task-nonce').val();
4301
4302 if (this.editingTaskId) {
4303 formData.task_id = this.editingTaskId;
4304 }
4305
4306 $.ajax({
4307 url: ajaxurl,
4308 type: 'POST',
4309 data: formData,
4310 success: function(response) {
4311 if (response.success) {
4312 // Show success message
4313 if (typeof WpForoAI !== 'undefined') {
4314 WpForoAI.showNotice(response.data.message || 'Task saved successfully.', 'success');
4315 }
4316
4317 // Reload the page to show updated task list
4318 setTimeout(function() {
4319 window.location.reload();
4320 }, 1000);
4321 } else {
4322 alert(response.data.message || 'Failed to save task.');
4323 $saveBtn.prop('disabled', false).html(originalText);
4324 }
4325 },
4326 error: function(xhr, status, error) {
4327 console.error('Save task error:', error);
4328 alert('Failed to save task. Please try again.');
4329 $saveBtn.prop('disabled', false).html(originalText);
4330 }
4331 });
4332 },
4333
4334 /**
4335 * Edit existing task
4336 */
4337 editTask: function(taskId) {
4338 const self = this;
4339
4340 // Show loading
4341 $('.wpforo-ai-task-edit').addClass('loading');
4342
4343 $.ajax({
4344 url: ajaxurl,
4345 type: 'POST',
4346 data: {
4347 action: 'wpforo_ai_get_task',
4348 task_id: taskId,
4349 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4350 _wpnonce: $('#wpforo-ai-task-nonce').val()
4351 },
4352 success: function(response) {
4353 $('.wpforo-ai-task-edit').removeClass('loading');
4354
4355 if (response.success && response.data.task) {
4356 self.populateForm(response.data.task);
4357 self.editingTaskId = taskId;
4358
4359 // Update form header
4360 $('.wpforo-ai-task-form-box .wpforo-ai-box-header h2').html(
4361 '<span class="dashicons dashicons-edit"></span> Edit Task: ' + response.data.task.task_name
4362 );
4363
4364 // Show form
4365 $('.wpforo-ai-task-form-container').addClass('visible');
4366 $('.wpforo-ai-create-task-btn').html('<span class="dashicons dashicons-no-alt"></span> Cancel');
4367
4368 // Scroll to form
4369 $('html, body').animate({
4370 scrollTop: $('.wpforo-ai-task-form-container').offset().top - 50
4371 }, 300);
4372 } else {
4373 alert(response.data.message || 'Failed to load task.');
4374 }
4375 },
4376 error: function() {
4377 $('.wpforo-ai-task-edit').removeClass('loading');
4378 alert('Failed to load task. Please try again.');
4379 }
4380 });
4381 },
4382
4383 /**
4384 * Populate form with task data
4385 */
4386 populateForm: function(task) {
4387 const self = this;
4388
4389 $('#wpforo-ai-task-name').val(task.task_name);
4390 $('#wpforo-ai-task-type').val(task.task_type).trigger('change');
4391
4392 // Set status radio button (not a select)
4393 $('input[name="status"][value="' + task.status + '"]').prop('checked', true);
4394
4395 // Parse config
4396 let config = {};
4397 try {
4398 config = typeof task.config === 'string' ? JSON.parse(task.config) : task.config;
4399 } catch (e) {
4400 console.error('Failed to parse task config:', e);
4401 }
4402
4403 // Set language dropdown (in basic section, always available in DOM)
4404 $('#wpforo-ai-task-language').val(config.response_language || '');
4405
4406 // Use setTimeout to ensure the template is fully rendered before populating
4407 // The trigger('change') loads the template HTML, but DOM needs time to update
4408 // Use 200ms to ensure reliable DOM rendering across all browsers
4409 setTimeout(function() {
4410 // Populate type-specific fields
4411 if (task.task_type === 'topic_generator') {
4412 self.populateTopicGeneratorConfig(config);
4413 } else if (task.task_type === 'reply_generator') {
4414 self.populateReplyGeneratorConfig(config);
4415 } else if (task.task_type === 'tag_maintenance') {
4416 self.populateTagMaintenanceConfig(config);
4417 }
4418 }, 200);
4419 },
4420
4421 /**
4422 * Populate Topic Generator config
4423 */
4424 populateTopicGeneratorConfig: function(config) {
4425 const $section = $('#wpforo-ai-task-config-section');
4426 const self = this;
4427
4428 // Check forum checkboxes (uncheck all first, then check saved ones)
4429 $section.find('[name="config[target_forum_ids][]"]').prop('checked', false);
4430 if (config.target_forums && config.target_forums.length > 0) {
4431 config.target_forums.forEach(function(forumId) {
4432 $section.find('[name="config[target_forum_ids][]"][value="' + forumId + '"]').prop('checked', true);
4433 });
4434 }
4435
4436 // Content settings
4437 $section.find('[name="config[topic_theme]"]').val(config.topic_theme || '');
4438 $section.find('[name="config[topic_style]"]').val(config.topic_style || 'neutral');
4439 $section.find('[name="config[topic_tone]"]').val(config.topic_tone || 'neutral');
4440 $section.find('[name="config[content_length]"]').val(config.content_length || 'medium');
4441
4442 // Content options (what to include)
4443 $section.find('[name="config[include_code]"]').prop('checked', config.include_code === true);
4444 $section.find('[name="config[include_links]"]').prop('checked', config.include_links === true);
4445 $section.find('[name="config[include_steps]"]').prop('checked', config.include_steps === true);
4446 $section.find('[name="config[include_youtube]"]').prop('checked', config.include_youtube === true);
4447
4448 // Author settings - handle both author_userid and legacy bot_user_id
4449 const authorUserId = config.author_userid || config.bot_user_id || '';
4450 $section.find('[name="config[author_userid]"]').val(authorUserId);
4451 $section.find('[name="config[author_groupid]"]').val(config.author_groupid || '');
4452 $section.find('[name="config[show_ai_badge]"]').prop('checked', config.show_ai_badge !== false);
4453
4454 // Load user display name if author_userid is set
4455 if (authorUserId) {
4456 self.loadUserDisplayName(authorUserId, $section);
4457 }
4458
4459 // Scheduling
4460 $section.find('[name="config[frequency]"]').val(config.frequency || 'daily');
4461 $section.find('[name="config[topics_per_run]"]').val(config.topics_per_run || 1);
4462
4463 // Active days - uncheck all first, then check saved ones
4464 if (config.active_days && config.active_days.length) {
4465 $section.find('[name="config[active_days][]"]').prop('checked', false);
4466 config.active_days.forEach(function(day) {
4467 $section.find('[name="config[active_days][]"][value="' + day + '"]').prop('checked', true);
4468 });
4469 }
4470
4471 // AI Quality & Credits
4472 $section.find('[name="config[quality_tier]"]').val(config.quality_tier || 'balanced');
4473 $section.find('[name="config[credit_stop_threshold]"]').val(config.credit_stop_threshold || 100);
4474 $section.find('[name="config[auto_pause_on_limit]"]').prop('checked', config.auto_pause_on_limit !== false);
4475
4476 // Content Safety
4477 $section.find('[name="config[duplicate_prevention]"]').prop('checked', config.duplicate_prevention !== false);
4478 $section.find('[name="config[similarity_threshold]"]').val(config.similarity_threshold || 75);
4479 $section.find('[name="config[duplicate_check_days]"]').val(config.duplicate_check_days || 90);
4480 $section.find('[name="config[topic_status]"][value="' + (config.topic_status || 0) + '"]').prop('checked', true);
4481
4482 // Advanced Options
4483 $section.find('[name="config[topic_prefix]"]').val(config.topic_prefix || '');
4484 $section.find('[name="config[topic_prefix_id]"]').val(config.topic_prefix_id || '');
4485 $section.find('[name="config[auto_tags]"]').val(config.auto_tags || '');
4486 $section.find('[name="config[search_keywords]"]').val(config.search_keywords || '');
4487
4488 // Update range slider display
4489 $section.find('.wpforo-ai-range-slider').each(function() {
4490 const $slider = $(this);
4491 const $valueDisplay = $slider.next('.wpforo-ai-range-value');
4492 if ($valueDisplay.length) {
4493 $valueDisplay.text($slider.val() + '%');
4494 }
4495 });
4496
4497 // Update duplicate prevention visibility
4498 const $duplicateCheckbox = $section.find('[name="config[duplicate_prevention]"]');
4499 const $duplicateSettings = $section.find('.wpforo-ai-duplicate-settings');
4500 if ($duplicateCheckbox.is(':checked')) {
4501 $duplicateSettings.show();
4502 } else {
4503 $duplicateSettings.hide();
4504 }
4505
4506 // Trigger event to update character counters after populating form
4507 $(document).trigger('wpforo-ai-task-loaded');
4508
4509 // Update estimated credits after populating form
4510 this.updateEstimatedCredits();
4511 },
4512
4513 /**
4514 * Load user display name for the user search field
4515 */
4516 loadUserDisplayName: function(userId, $section) {
4517 if (!userId) return;
4518
4519 $.ajax({
4520 url: ajaxurl,
4521 type: 'POST',
4522 data: {
4523 action: 'wpforo_ai_search_users',
4524 search: '',
4525 user_id: userId,
4526 _wpnonce: $('#wpforo-ai-task-nonce').val()
4527 },
4528 success: function(response) {
4529 if (response.success && response.data.users && response.data.users.length > 0) {
4530 const user = response.data.users[0];
4531 $section.find('.wpforo-ai-user-search').val(user.label);
4532 }
4533 }
4534 });
4535 },
4536
4537 /**
4538 * Populate Reply Generator config
4539 */
4540 populateReplyGeneratorConfig: function(config) {
4541 const $section = $('#wpforo-ai-task-config-section');
4542 const self = this;
4543
4544 // Target settings - forums (uncheck all first, then check saved ones)
4545 $section.find('[name="config[reply_target_forum_ids][]"]').prop('checked', false);
4546 if (config.reply_target_forums && config.reply_target_forums.length > 0) {
4547 config.reply_target_forums.forEach(function(forumId) {
4548 $section.find('[name="config[reply_target_forum_ids][]"][value="' + forumId + '"]').prop('checked', true);
4549 });
4550 }
4551
4552 // Target settings - topic IDs and date range
4553 $section.find('[name="config[target_topic_ids]"]').val(config.target_topic_ids || '');
4554 $section.find('[name="config[only_not_replied]"]').prop('checked', config.only_not_replied === true);
4555 $section.find('[name="config[date_range_from]"]').val(config.date_range_from || '');
4556 $section.find('[name="config[date_range_to]"]').val(config.date_range_to || '');
4557
4558 // Reply content settings
4559 $section.find('[name="config[reply_style]"]').val(config.reply_style || 'neutral');
4560 $section.find('[name="config[reply_tone]"]').val(config.reply_tone || 'neutral');
4561 $section.find('[name="config[response_guidelines]"]').val(config.response_guidelines || '');
4562 $section.find('[name="config[reply_length]"]').val(config.reply_length || 'medium');
4563 $section.find('[name="config[knowledge_source]"]').val(config.knowledge_source || 'forum_only');
4564 $section.find('[name="config[no_content_action]"]').val(config.no_content_action || 'use_ai_fallback');
4565
4566 // Author settings - handle both author_userid and legacy bot_user_id
4567 const authorUserId = config.author_userid || config.bot_user_id || '';
4568 $section.find('[name="config[author_userid]"]').val(authorUserId);
4569 $section.find('[name="config[author_groupid]"]').val(config.author_groupid || '');
4570 $section.find('[name="config[show_ai_badge]"]').prop('checked', config.show_ai_badge !== false);
4571
4572 // Load user display name if author_userid is set
4573 if (authorUserId) {
4574 self.loadUserDisplayName(authorUserId, $section);
4575 }
4576
4577 $section.find('[name="config[frequency]"]').val(config.frequency || '3hours');
4578 $section.find('[name="config[replies_per_run]"]').val(config.replies_per_run || 3);
4579 $section.find('[name="config[quality_tier]"]').val(config.quality_tier || 'balanced');
4580 $section.find('[name="config[credit_stop_threshold]"]').val(config.credit_stop_threshold || 100);
4581 $section.find('[name="config[auto_pause_on_limit]"]').prop('checked', config.auto_pause_on_limit !== false);
4582 $section.find('[name="config[duplicate_prevention]"]').prop('checked', config.duplicate_prevention !== false);
4583 $section.find('[name="config[similarity_threshold]"]').val(config.similarity_threshold || 75);
4584 $section.find('[name="config[duplicate_check_days]"]').val(config.duplicate_check_days || 90);
4585 $section.find('[name="config[reply_status]"][value="' + (config.reply_status || 0) + '"]').prop('checked', true);
4586 $section.find('[name="config[reply_strategy]"]').val(config.reply_strategy || 'first_post');
4587
4588 // Reply content options
4589 $section.find('[name="config[reply_include_code]"]').prop('checked', config.reply_include_code === true);
4590 $section.find('[name="config[reply_include_links]"]').prop('checked', config.reply_include_links === true);
4591 $section.find('[name="config[reply_include_steps]"]').prop('checked', config.reply_include_steps === true);
4592 $section.find('[name="config[reply_include_followup]"]').prop('checked', config.reply_include_followup !== false);
4593 $section.find('[name="config[reply_include_youtube]"]').prop('checked', config.reply_include_youtube === true);
4594 $section.find('[name="config[reply_include_greeting]"]').prop('checked', config.reply_include_greeting !== false);
4595 $section.find('[name="config[max_replies_per_topic]"]').val(config.max_replies_per_topic || 1);
4596
4597 // Check day checkboxes
4598 if (config.active_days && config.active_days.length) {
4599 // Uncheck all first
4600 $section.find('[name="config[active_days][]"]').prop('checked', false);
4601 config.active_days.forEach(function(day) {
4602 $section.find('[name="config[active_days][]"][value="' + day + '"]').prop('checked', true);
4603 });
4604 }
4605
4606 // Update range slider display
4607 $section.find('.wpforo-ai-range-slider').each(function() {
4608 const $slider = $(this);
4609 const $valueDisplay = $slider.next('.wpforo-ai-range-value');
4610 if ($valueDisplay.length) {
4611 $valueDisplay.text($slider.val() + '%');
4612 }
4613 });
4614
4615 // Update duplicate prevention visibility
4616 const $duplicateCheckbox = $section.find('[name="config[duplicate_prevention]"]');
4617 const $duplicateSettings = $section.find('.wpforo-ai-duplicate-settings');
4618 if ($duplicateCheckbox.is(':checked')) {
4619 $duplicateSettings.show();
4620 } else {
4621 $duplicateSettings.hide();
4622 }
4623
4624 // Run on approval toggle
4625 const $runOnApproval = $section.find('[name="config[run_on_approval]"]');
4626 const $scheduledOptions = $section.find('.wpforo-ai-scheduled-options');
4627 if (config.run_on_approval) {
4628 $runOnApproval.prop('checked', true);
4629 $scheduledOptions.hide();
4630 $scheduledOptions.find('input, select').prop('disabled', true);
4631 } else {
4632 $runOnApproval.prop('checked', false);
4633 $scheduledOptions.show();
4634 $scheduledOptions.find('input, select').prop('disabled', false);
4635 }
4636
4637 // Trigger event to update character counters after populating form
4638 $(document).trigger('wpforo-ai-task-loaded');
4639
4640 // Update estimated credits after populating form
4641 this.updateEstimatedCredits();
4642 },
4643
4644 /**
4645 * Populate Tag Maintenance config
4646 */
4647 populateTagMaintenanceConfig: function(config) {
4648 const $section = $('#wpforo-ai-task-config-section');
4649
4650 // Target settings - forums (uncheck all first, then check saved ones)
4651 $section.find('[name="config[tag_target_forum_ids][]"]').prop('checked', false);
4652 if (config.tag_target_forum_ids && config.tag_target_forum_ids.length > 0) {
4653 config.tag_target_forum_ids.forEach(function(forumId) {
4654 $section.find('[name="config[tag_target_forum_ids][]"][value="' + forumId + '"]').prop('checked', true);
4655 });
4656 }
4657
4658 // Target settings - topic IDs and date range
4659 $section.find('[name="config[target_topic_ids]"]').val(config.target_topic_ids || '');
4660 $section.find('[name="config[date_range_from]"]').val(config.date_range_from || '');
4661 $section.find('[name="config[date_range_to]"]').val(config.date_range_to || '');
4662 $section.find('[name="config[only_not_tagged]"]').prop('checked', config.only_not_tagged === true);
4663
4664 // Tag options
4665 $section.find('[name="config[max_tags]"]').val(config.max_tags || 5);
4666 $section.find('[name="config[preserve_existing]"]').prop('checked', config.preserve_existing !== false);
4667 $section.find('[name="config[maintain_vocabulary]"]').prop('checked', config.maintain_vocabulary !== false);
4668 $section.find('[name="config[remove_duplicates]"]').prop('checked', config.remove_duplicates !== false);
4669 $section.find('[name="config[remove_irrelevant]"]').prop('checked', config.remove_irrelevant !== false);
4670 $section.find('[name="config[lowercase]"]').prop('checked', config.lowercase === true);
4671
4672 // Scheduling
4673 $section.find('[name="config[frequency]"]').val(config.frequency || 'daily');
4674 $section.find('[name="config[topics_per_run]"]').val(config.topics_per_run || 20);
4675
4676 // Active days - uncheck all first, then check saved ones
4677 if (config.active_days && config.active_days.length) {
4678 $section.find('[name="config[active_days][]"]').prop('checked', false);
4679 config.active_days.forEach(function(day) {
4680 $section.find('[name="config[active_days][]"][value="' + day + '"]').prop('checked', true);
4681 });
4682 }
4683
4684 // AI Quality & Credits
4685 $section.find('[name="config[quality_tier]"]').val(config.quality_tier || 'premium');
4686 $section.find('[name="config[credit_stop_threshold]"]').val(config.credit_stop_threshold || 500);
4687 $section.find('[name="config[auto_pause_on_limit]"]').prop('checked', config.auto_pause_on_limit !== false);
4688
4689 // Run on approval toggle
4690 const $runOnApproval = $section.find('[name="config[run_on_approval]"]');
4691 const $scheduledOptions = $section.find('.wpforo-ai-scheduled-options');
4692 if (config.run_on_approval) {
4693 $runOnApproval.prop('checked', true);
4694 $scheduledOptions.hide();
4695 $scheduledOptions.find('input, select').prop('disabled', true);
4696 } else {
4697 $runOnApproval.prop('checked', false);
4698 $scheduledOptions.show();
4699 $scheduledOptions.find('input, select').prop('disabled', false);
4700 }
4701
4702 // Trigger event to update character counters after populating form
4703 $(document).trigger('wpforo-ai-task-loaded');
4704
4705 // Update estimated credits after populating form
4706 this.updateEstimatedCredits();
4707 },
4708
4709 /**
4710 * Delete task
4711 */
4712 deleteTask: function(taskId) {
4713 if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) {
4714 return;
4715 }
4716
4717 const $row = $('tr[data-task-id="' + taskId + '"]');
4718 $row.css('opacity', '0.5');
4719
4720 $.ajax({
4721 url: ajaxurl,
4722 type: 'POST',
4723 data: {
4724 action: 'wpforo_ai_delete_task',
4725 task_id: taskId,
4726 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4727 _wpnonce: $('#wpforo-ai-task-nonce').val()
4728 },
4729 success: function(response) {
4730 if (response.success) {
4731 $row.fadeOut(300, function() {
4732 $(this).remove();
4733
4734 // Show empty state if no tasks left
4735 if ($('.wpforo-ai-tasks-table tbody tr').length === 0) {
4736 $('.wpforo-ai-tasks-table').replaceWith(
4737 '<div class="wpforo-ai-tasks-empty">' +
4738 '<span class="dashicons dashicons-schedule"></span>' +
4739 '<h3>No AI Tasks Yet</h3>' +
4740 '<p>Create your first AI task to automate forum content generation.</p>' +
4741 '</div>'
4742 );
4743 }
4744 });
4745
4746 if (typeof WpForoAI !== 'undefined') {
4747 WpForoAI.showNotice('Task deleted successfully.', 'success');
4748 }
4749 } else {
4750 $row.css('opacity', '1');
4751 alert(response.data.message || 'Failed to delete task.');
4752 }
4753 },
4754 error: function() {
4755 $row.css('opacity', '1');
4756 alert('Failed to delete task. Please try again.');
4757 }
4758 });
4759 },
4760
4761 /**
4762 * Run task immediately
4763 */
4764 runTask: function(taskId) {
4765 if (!confirm('Run this task now? This will use credits from your account.')) {
4766 return;
4767 }
4768
4769 const $row = $('tr[data-task-id="' + taskId + '"]');
4770 const $statusCell = $row.find('td.column-status');
4771 const originalStatusHtml = $statusCell.html();
4772
4773 // Show running indicator on the row - replace status content
4774 $row.addClass('wpforo-ai-task-running');
4775 $statusCell.html('<span class="wpforo-ai-task-status status-running"><span class="dashicons dashicons-update wpforo-ai-spin"></span> Running...</span>');
4776
4777 // Disable all action buttons for this task
4778 $row.find('.wpforo-ai-task-actions button').prop('disabled', true);
4779
4780 $.ajax({
4781 url: ajaxurl,
4782 type: 'POST',
4783 data: {
4784 action: 'wpforo_ai_run_task',
4785 task_id: taskId,
4786 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4787 _wpnonce: $('#wpforo-ai-task-nonce').val()
4788 },
4789 success: function(response) {
4790 $row.removeClass('wpforo-ai-task-running');
4791 $row.find('.wpforo-ai-task-actions button').prop('disabled', false);
4792
4793 if (response.success) {
4794 // Show success status briefly
4795 $statusCell.html('<span class="wpforo-ai-task-status status-success"><span class="dashicons dashicons-yes-alt"></span> Completed</span>');
4796
4797 if (typeof WpForoAI !== 'undefined') {
4798 WpForoAI.showNotice(response.data.message || 'Task completed successfully.', 'success');
4799 }
4800
4801 // Reload to show updated stats
4802 setTimeout(function() {
4803 window.location.reload();
4804 }, 1500);
4805 } else {
4806 // Restore original status on error
4807 $statusCell.html(originalStatusHtml);
4808 if (typeof WpForoAI !== 'undefined') {
4809 WpForoAI.showNotice(response.data.message || 'Failed to run task.', 'error');
4810 } else {
4811 alert(response.data.message || 'Failed to run task.');
4812 }
4813 }
4814 },
4815 error: function() {
4816 $row.removeClass('wpforo-ai-task-running');
4817 $row.find('.wpforo-ai-task-actions button').prop('disabled', false);
4818 $statusCell.html(originalStatusHtml);
4819 if (typeof WpForoAI !== 'undefined') {
4820 WpForoAI.showNotice('Failed to run task. Please try again.', 'error');
4821 } else {
4822 alert('Failed to run task. Please try again.');
4823 }
4824 }
4825 });
4826 },
4827
4828 /**
4829 * Toggle task status (pause/resume/activate)
4830 */
4831 toggleTaskStatus: function(taskId, newStatus) {
4832 const self = this;
4833 const $row = $('tr[data-task-id="' + taskId + '"]');
4834 const $statusBadge = $row.find('.wpforo-ai-task-status');
4835
4836 // If no status provided, toggle between active and paused
4837 if (!newStatus) {
4838 const currentStatus = $statusBadge.hasClass('status-active') ? 'active' : 'paused';
4839 newStatus = currentStatus === 'active' ? 'paused' : 'active';
4840 }
4841
4842 $.ajax({
4843 url: ajaxurl,
4844 type: 'POST',
4845 data: {
4846 action: 'wpforo_ai_update_task_status',
4847 task_id: taskId,
4848 status: newStatus,
4849 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4850 _wpnonce: $('#wpforo-ai-task-nonce').val()
4851 },
4852 success: function(response) {
4853 if (response.success) {
4854 // Update status badge
4855 $statusBadge.removeClass('status-active status-paused status-draft');
4856 $statusBadge.addClass('status-' + newStatus);
4857
4858 if (newStatus === 'active') {
4859 $statusBadge.html('<span class="dashicons dashicons-yes-alt"></span> Active');
4860 } else {
4861 $statusBadge.html('<span class="dashicons dashicons-clock"></span> Paused');
4862 }
4863
4864 // Update dropdown buttons - swap activate/pause
4865 const $dropdown = $row.find('.wpforo-ai-task-actions-menu');
4866 const $activateBtn = $dropdown.find('.wpforo-ai-task-activate');
4867 const $pauseBtn = $dropdown.find('.wpforo-ai-task-pause');
4868
4869 if (newStatus === 'active') {
4870 // Replace activate with pause
4871 if ($activateBtn.length) {
4872 $activateBtn
4873 .removeClass('wpforo-ai-task-activate')
4874 .addClass('wpforo-ai-task-pause')
4875 .html('<span class="dashicons dashicons-controls-pause"></span> Pause');
4876 }
4877 } else {
4878 // Replace pause with activate
4879 if ($pauseBtn.length) {
4880 $pauseBtn
4881 .removeClass('wpforo-ai-task-pause')
4882 .addClass('wpforo-ai-task-activate')
4883 .html('<span class="dashicons dashicons-controls-play"></span> Activate');
4884 }
4885 }
4886
4887 // Update row data attribute
4888 $row.data('status', newStatus);
4889
4890 if (typeof WpForoAI !== 'undefined') {
4891 WpForoAI.showNotice('Task ' + (newStatus === 'active' ? 'activated' : 'paused') + ' successfully.', 'success');
4892 }
4893 } else {
4894 alert(response.data.message || 'Failed to update task status.');
4895 }
4896 },
4897 error: function() {
4898 alert('Failed to update task status. Please try again.');
4899 }
4900 });
4901 },
4902
4903 /**
4904 * Duplicate a task
4905 */
4906 duplicateTask: function(taskId) {
4907 const self = this;
4908
4909 $.ajax({
4910 url: ajaxurl,
4911 type: 'POST',
4912 data: {
4913 action: 'wpforo_ai_duplicate_task',
4914 task_id: taskId,
4915 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4916 _wpnonce: $('#wpforo-ai-task-nonce').val()
4917 },
4918 success: function(response) {
4919 if (response.success) {
4920 if (typeof WpForoAI !== 'undefined') {
4921 WpForoAI.showNotice('Task duplicated successfully.', 'success');
4922 }
4923 // Reload page to show new task
4924 window.location.reload();
4925 } else {
4926 alert(response.data.message || 'Failed to duplicate task.');
4927 }
4928 },
4929 error: function() {
4930 alert('Failed to duplicate task. Please try again.');
4931 }
4932 });
4933 },
4934
4935 /**
4936 * View task statistics
4937 */
4938 viewTaskStats: function(taskId) {
4939 const self = this;
4940 const $row = $('tr[data-task-id="' + taskId + '"]');
4941 const taskName = $row.find('.wpforo-ai-task-name').text().trim();
4942
4943 $.ajax({
4944 url: ajaxurl,
4945 type: 'POST',
4946 data: {
4947 action: 'wpforo_ai_get_task_stats',
4948 task_id: taskId,
4949 board_id: $('#wpforo-ai-task-board-id').val() || 0,
4950 _wpnonce: $('#wpforo-ai-task-nonce').val()
4951 },
4952 success: function(response) {
4953 if (response.success) {
4954 self.showStatsModal(taskName, response.data.stats);
4955 } else {
4956 alert(response.data.message || 'Failed to load task statistics.');
4957 }
4958 },
4959 error: function() {
4960 alert('Failed to load task statistics. Please try again.');
4961 }
4962 });
4963 },
4964
4965 /**
4966 * Show statistics modal
4967 */
4968 showStatsModal: function(taskName, stats) {
4969 // Remove existing modal
4970 $('.wpforo-ai-stats-modal-overlay').remove();
4971
4972 const modalHtml = `
4973 <div class="wpforo-ai-stats-modal-overlay">
4974 <div class="wpforo-ai-stats-modal">
4975 <div class="wpforo-ai-stats-modal-header">
4976 <h3><span class="dashicons dashicons-chart-bar"></span> Task Statistics: ${taskName}</h3>
4977 <button type="button" class="wpforo-ai-stats-modal-close">&times;</button>
4978 </div>
4979 <div class="wpforo-ai-stats-modal-body">
4980 <div class="wpforo-ai-stats-grid">
4981 <div class="wpforo-ai-stat-card">
4982 <div class="wpforo-ai-stat-value">${stats.total_runs || 0}</div>
4983 <div class="wpforo-ai-stat-label">Total Runs</div>
4984 </div>
4985 <div class="wpforo-ai-stat-card">
4986 <div class="wpforo-ai-stat-value">${stats.items_created || 0}</div>
4987 <div class="wpforo-ai-stat-label">Number of Items</div>
4988 </div>
4989 <div class="wpforo-ai-stat-card">
4990 <div class="wpforo-ai-stat-value">${stats.credits_used || 0}</div>
4991 <div class="wpforo-ai-stat-label">Credits Used</div>
4992 </div>
4993 <div class="wpforo-ai-stat-card">
4994 <div class="wpforo-ai-stat-value">${stats.success_rate || '0%'}</div>
4995 <div class="wpforo-ai-stat-label">Success Rate</div>
4996 </div>
4997 </div>
4998 <div class="wpforo-ai-stats-details">
4999 <p><strong>Last Run:</strong> ${stats.last_run || 'Never'}</p>
5000 <p><strong>Next Scheduled:</strong> ${stats.next_run || 'Not scheduled'}</p>
5001 <p><strong>Avg Items/Run:</strong> ${stats.avg_items_per_run || '0'}</p>
5002 </div>
5003 </div>
5004 </div>
5005 </div>
5006 `;
5007
5008 $('body').append(modalHtml);
5009
5010 // Close modal events
5011 $('.wpforo-ai-stats-modal-close, .wpforo-ai-stats-modal-overlay').on('click', function(e) {
5012 if (e.target === this) {
5013 $('.wpforo-ai-stats-modal-overlay').remove();
5014 }
5015 });
5016 },
5017
5018 /**
5019 * View task logs
5020 */
5021 viewTaskLogs: function(taskId) {
5022 const self = this;
5023 const $row = $('tr[data-task-id="' + taskId + '"]');
5024 const taskName = $row.find('.wpforo-ai-task-name').text().trim();
5025
5026 $.ajax({
5027 url: ajaxurl,
5028 type: 'POST',
5029 data: {
5030 action: 'wpforo_ai_get_task_logs',
5031 task_id: taskId,
5032 limit: 50,
5033 board_id: $('#wpforo-ai-task-board-id').val() || 0,
5034 _wpnonce: $('#wpforo-ai-task-nonce').val()
5035 },
5036 success: function(response) {
5037 if (response.success) {
5038 self.showLogsModal(taskName, response.data.logs);
5039 } else {
5040 alert(response.data.message || 'Failed to load task logs.');
5041 }
5042 },
5043 error: function() {
5044 alert('Failed to load task logs. Please try again.');
5045 }
5046 });
5047 },
5048
5049 /**
5050 * Show logs modal
5051 */
5052 showLogsModal: function(taskName, logs) {
5053 // Remove existing modal
5054 $('.wpforo-ai-logs-modal-overlay').remove();
5055
5056 let logsHtml = '';
5057 if (logs && logs.length > 0) {
5058 logsHtml = '<table class="wpforo-ai-logs-table"><thead><tr>' +
5059 '<th>Date</th><th>Status</th><th>Items</th><th>Credits</th><th>Duration</th><th>Message</th>' +
5060 '</tr></thead><tbody>';
5061
5062 logs.forEach(function(log) {
5063 const statusClass = log.status === 'completed' ? 'status-success' :
5064 (log.status === 'error' ? 'status-error' : 'status-warning');
5065 const duration = log.execution_duration ? parseFloat(log.execution_duration).toFixed(1) + 's' : '-';
5066 const message = log.error_message || (log.status === 'completed' ? 'Success' : '-');
5067 logsHtml += '<tr>' +
5068 '<td>' + (log.execution_time || '-') + '</td>' +
5069 '<td><span class="wpforo-ai-log-status ' + statusClass + '">' + (log.status || '-') + '</span></td>' +
5070 '<td>' + (log.items_created || 0) + '</td>' +
5071 '<td>' + (log.credits_used || 0) + '</td>' +
5072 '<td>' + duration + '</td>' +
5073 '<td>' + message + '</td>' +
5074 '</tr>';
5075 });
5076
5077 logsHtml += '</tbody></table>';
5078 } else {
5079 logsHtml = '<div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-info-outline"></span><p>No logs found for this task yet.</p></div>';
5080 }
5081
5082 const modalHtml = `
5083 <div class="wpforo-ai-logs-modal-overlay">
5084 <div class="wpforo-ai-logs-modal">
5085 <div class="wpforo-ai-logs-modal-header">
5086 <h3><span class="dashicons dashicons-list-view"></span> Task Logs: ${taskName}</h3>
5087 <button type="button" class="wpforo-ai-logs-modal-close">&times;</button>
5088 </div>
5089 <div class="wpforo-ai-logs-modal-body">
5090 ${logsHtml}
5091 </div>
5092 </div>
5093 </div>
5094 `;
5095
5096 $('body').append(modalHtml);
5097
5098 // Close modal events
5099 $('.wpforo-ai-logs-modal-close, .wpforo-ai-logs-modal-overlay').on('click', function(e) {
5100 if (e.target === this) {
5101 $('.wpforo-ai-logs-modal-overlay').remove();
5102 }
5103 });
5104 },
5105
5106 /**
5107 * Apply bulk action
5108 */
5109 applyBulkAction: function() {
5110 const action = $('.wpforo-ai-bulk-action-select').val();
5111 const selectedIds = [];
5112
5113 $('.wpforo-ai-task-checkbox:checked').each(function() {
5114 selectedIds.push($(this).val());
5115 });
5116
5117 if (!action) {
5118 alert('Please select a bulk action.');
5119 return;
5120 }
5121
5122 if (selectedIds.length === 0) {
5123 alert('Please select at least one task.');
5124 return;
5125 }
5126
5127 let confirmMessage = 'Are you sure you want to ' + action + ' ' + selectedIds.length + ' task(s)?';
5128 if (action === 'delete') {
5129 confirmMessage = 'Are you sure you want to delete ' + selectedIds.length + ' task(s)? This action cannot be undone.';
5130 }
5131
5132 if (!confirm(confirmMessage)) {
5133 return;
5134 }
5135
5136 $.ajax({
5137 url: ajaxurl,
5138 type: 'POST',
5139 data: {
5140 action: 'wpforo_ai_bulk_task_action',
5141 bulk_action: action,
5142 task_ids: selectedIds,
5143 board_id: $('#wpforo-ai-task-board-id').val() || 0,
5144 _wpnonce: $('#wpforo-ai-task-nonce').val()
5145 },
5146 success: function(response) {
5147 if (response.success) {
5148 if (typeof WpForoAI !== 'undefined') {
5149 WpForoAI.showNotice(response.data.message || 'Bulk action completed.', 'success');
5150 }
5151 window.location.reload();
5152 } else {
5153 alert(response.data.message || 'Bulk action failed.');
5154 }
5155 },
5156 error: function() {
5157 alert('Bulk action failed. Please try again.');
5158 }
5159 });
5160 },
5161
5162 /**
5163 * Filter tasks
5164 */
5165 filterTasks: function() {
5166 const status = $('.wpforo-ai-filter-status').val();
5167 const type = $('.wpforo-ai-filter-type').val();
5168 const search = $('.wpforo-ai-search-tasks').val().toLowerCase();
5169
5170 $('.wpforo-ai-tasks-table tbody tr').each(function() {
5171 const $row = $(this);
5172 const rowStatus = $row.data('status');
5173 const rowType = $row.data('task-type');
5174 const rowName = $row.find('.wpforo-ai-task-name').text().toLowerCase();
5175
5176 let visible = true;
5177
5178 if (status && rowStatus !== status) {
5179 visible = false;
5180 }
5181
5182 if (type && rowType !== type) {
5183 visible = false;
5184 }
5185
5186 if (search && rowName.indexOf(search) === -1) {
5187 visible = false;
5188 }
5189
5190 $row.toggle(visible);
5191 });
5192 }
5193 };
5194
5195 // Initialize AI Tasks if on that tab
5196 if ($('.wpforo-ai-tasks-tab').length) {
5197 WpForoAITasks.init();
5198 }
5199
5200 // Make available globally
5201 window.WpForoAITasks = WpForoAITasks;
5202
5203 // ==========================================================================
5204 // Analytics Tab
5205 // ==========================================================================
5206
5207 const WpForoAIAnalytics = {
5208 charts: {},
5209 data: null,
5210 initialized: false,
5211
5212 init: function() {
5213 if (this.initialized) {
5214 return;
5215 }
5216 this.initialized = true;
5217 this.bindEvents();
5218 this.loadAnalyticsData();
5219 },
5220
5221 destroyAllCharts: function() {
5222 // Destroy charts stored in our object
5223 Object.keys(this.charts).forEach(key => {
5224 if (this.charts[key]) {
5225 this.charts[key].destroy();
5226 this.charts[key] = null;
5227 }
5228 });
5229
5230 // Also destroy any Chart.js charts on our canvases
5231 ['credits-usage-chart', 'credits-by-feature-chart', 'moderation-stats-chart'].forEach(id => {
5232 const canvas = document.getElementById(id);
5233 if (canvas) {
5234 const existingChart = Chart.getChart(canvas);
5235 if (existingChart) {
5236 existingChart.destroy();
5237 }
5238 }
5239 });
5240 },
5241
5242 bindEvents: function() {
5243 // Custom range toggle - use .off() to prevent duplicate bindings
5244 $('.wpforo-ai-custom-range-toggle').off('click.customRange').on('click.customRange', function(e) {
5245 e.preventDefault();
5246 e.stopPropagation();
5247 $('.wpforo-ai-custom-range-picker').slideToggle(200);
5248 });
5249 },
5250
5251 loadAnalyticsData: function() {
5252 if (typeof wpforoAIAnalytics === 'undefined') {
5253 return;
5254 }
5255
5256 const self = this;
5257
5258 $.ajax({
5259 url: wpforoAIAnalytics.ajaxUrl,
5260 type: 'POST',
5261 data: {
5262 action: 'wpforo_ai_get_analytics',
5263 nonce: wpforoAIAnalytics.nonce,
5264 board_id: wpforoAIAnalytics.boardId,
5265 start_time: wpforoAIAnalytics.startTime,
5266 end_time: wpforoAIAnalytics.endTime
5267 },
5268 success: function(response) {
5269 if (response.success && response.data) {
5270 self.data = response.data;
5271 self.updateSummaryCards();
5272 self.renderCharts();
5273 self.renderUsageTable();
5274 } else {
5275 self.showError(response.data ? response.data.message : wpforoAIAnalytics.i18n.error);
5276 }
5277 },
5278 error: function() {
5279 self.showError(wpforoAIAnalytics.i18n.error);
5280 }
5281 });
5282 },
5283
5284 updateSummaryCards: function() {
5285 const summary = this.data.summary || {};
5286
5287 $('#total-credits-used').text(this.formatNumber(summary.total_credits || 0));
5288 $('#avg-credits-day').text(this.formatNumber(summary.avg_credits_per_day || 0, 1));
5289 $('#total-api-calls').text(this.formatNumber(summary.total_requests || 0));
5290 $('#success-rate').text((summary.success_rate || 0).toFixed(1) + '%');
5291 },
5292
5293 renderCharts: function() {
5294 this.hideLoading();
5295 this.destroyAllCharts();
5296 this.renderCreditsOverTimeChart();
5297 this.renderCreditsByFeatureChart();
5298 this.renderModerationChart();
5299 },
5300
5301 renderCreditsOverTimeChart: function() {
5302 const ctx = document.getElementById('credits-usage-chart');
5303 if (!ctx) return;
5304
5305 const timeSeries = this.data.time_series || [];
5306
5307 if (timeSeries.length === 0) {
5308 this.showNoData(ctx.parentElement);
5309 return;
5310 }
5311
5312 const showYear = timeSeries.length > 1 && new Date(timeSeries[0].date).getFullYear() !== new Date(timeSeries[timeSeries.length - 1].date).getFullYear();
5313 const labels = timeSeries.map(function(item) {
5314 const date = new Date(item.date);
5315 const opts = showYear ? { month: 'short', day: 'numeric', year: '2-digit' } : { month: 'short', day: 'numeric' };
5316 return date.toLocaleDateString(undefined, opts);
5317 });
5318 const credits = timeSeries.map(item => item.credits);
5319 const requests = timeSeries.map(item => item.requests);
5320
5321 if (this.charts.creditsOverTime) {
5322 this.charts.creditsOverTime.destroy();
5323 }
5324
5325 this.charts.creditsOverTime = new Chart(ctx, {
5326 type: 'line',
5327 data: {
5328 labels: labels,
5329 datasets: [
5330 {
5331 label: wpforoAIAnalytics.i18n.credits,
5332 data: credits,
5333 borderColor: '#2271b1',
5334 backgroundColor: 'rgba(34, 113, 177, 0.1)',
5335 fill: true,
5336 tension: 0.4,
5337 yAxisID: 'y'
5338 },
5339 {
5340 label: wpforoAIAnalytics.i18n.requests,
5341 data: requests,
5342 borderColor: '#46b450',
5343 backgroundColor: 'transparent',
5344 borderDash: [5, 5],
5345 tension: 0.4,
5346 yAxisID: 'y1'
5347 }
5348 ]
5349 },
5350 options: {
5351 responsive: true,
5352 maintainAspectRatio: false,
5353 interaction: {
5354 mode: 'index',
5355 intersect: false
5356 },
5357 plugins: {
5358 legend: {
5359 position: 'top'
5360 }
5361 },
5362 scales: {
5363 y: {
5364 type: 'linear',
5365 display: true,
5366 position: 'left',
5367 title: {
5368 display: true,
5369 text: wpforoAIAnalytics.i18n.credits
5370 }
5371 },
5372 y1: {
5373 type: 'linear',
5374 display: true,
5375 position: 'right',
5376 title: {
5377 display: true,
5378 text: wpforoAIAnalytics.i18n.requests
5379 },
5380 grid: {
5381 drawOnChartArea: false
5382 }
5383 }
5384 }
5385 }
5386 });
5387 },
5388
5389 renderCreditsByFeatureChart: function() {
5390 const ctx = document.getElementById('credits-by-feature-chart');
5391 if (!ctx) return;
5392
5393 const byFeature = this.data.by_feature || {};
5394 const features = Object.keys(byFeature);
5395
5396 if (features.length === 0) {
5397 this.showNoData(ctx.parentElement);
5398 return;
5399 }
5400
5401 const labels = features.map(key => wpforoAIAnalytics.featureNames[key] || key);
5402 const data = features.map(key => byFeature[key].credits || 0);
5403 const colors = features.map(key => wpforoAIAnalytics.featureColors[key] || '#999');
5404
5405 if (this.charts.creditsByFeature) {
5406 this.charts.creditsByFeature.destroy();
5407 }
5408
5409 this.charts.creditsByFeature = new Chart(ctx, {
5410 type: 'doughnut',
5411 data: {
5412 labels: labels,
5413 datasets: [{
5414 data: data,
5415 backgroundColor: colors,
5416 borderWidth: 2,
5417 borderColor: '#fff'
5418 }]
5419 },
5420 options: {
5421 responsive: true,
5422 maintainAspectRatio: false,
5423 plugins: {
5424 legend: {
5425 position: 'right',
5426 labels: {
5427 boxWidth: 12,
5428 padding: 15
5429 }
5430 }
5431 }
5432 }
5433 });
5434 },
5435
5436 renderModerationChart: function() {
5437 const ctx = document.getElementById('moderation-stats-chart');
5438 if (!ctx) return;
5439
5440 const moderation = this.data.moderation || {};
5441
5442 const labels = [
5443 wpforoAIAnalytics.i18n.spamBlocked,
5444 wpforoAIAnalytics.i18n.toxicDetected,
5445 wpforoAIAnalytics.i18n.policyViolations,
5446 wpforoAIAnalytics.i18n.cleanPassed
5447 ];
5448
5449 const data = [
5450 moderation.spam_blocked || 0,
5451 moderation.toxic_detected || 0,
5452 moderation.policy_violations || 0,
5453 moderation.clean_passed || 0
5454 ];
5455
5456 const colors = ['#F44336', '#E91E63', '#673AB7', '#46b450'];
5457
5458 if (this.charts.moderation) {
5459 this.charts.moderation.destroy();
5460 }
5461
5462 this.charts.moderation = new Chart(ctx, {
5463 type: 'bar',
5464 data: {
5465 labels: labels,
5466 datasets: [{
5467 data: data,
5468 backgroundColor: colors,
5469 borderRadius: 4
5470 }]
5471 },
5472 options: {
5473 responsive: true,
5474 maintainAspectRatio: false,
5475 plugins: {
5476 legend: {
5477 display: false
5478 }
5479 },
5480 scales: {
5481 y: {
5482 beginAtZero: true,
5483 ticks: {
5484 stepSize: 1
5485 }
5486 }
5487 }
5488 }
5489 });
5490 },
5491
5492 renderUsageTable: function() {
5493 const tbody = $('#feature-usage-tbody');
5494 if (!tbody.length) return;
5495
5496 const byFeature = this.data.by_feature || {};
5497 const features = Object.keys(byFeature);
5498
5499 if (features.length === 0) {
5500 tbody.html('<tr class="wpforo-ai-no-data"><td colspan="5">' + wpforoAIAnalytics.i18n.noData + '</td></tr>');
5501 return;
5502 }
5503
5504 let html = '';
5505 features.forEach(key => {
5506 const feature = byFeature[key];
5507 const name = wpforoAIAnalytics.featureNames[key] || key;
5508 const color = wpforoAIAnalytics.featureColors[key] || '#999';
5509 const successRate = feature.success_rate || 100;
5510 const rateClass = successRate >= 95 ? '' : (successRate >= 80 ? 'warning' : 'error');
5511
5512 html += '<tr>';
5513 html += '<td><div class="wpforo-ai-feature-name"><span class="wpforo-ai-feature-color" style="background-color: ' + color + '"></span>' + name + '</div></td>';
5514 html += '<td>' + this.formatNumber(feature.requests || 0) + '</td>';
5515 html += '<td>' + this.formatNumber(feature.credits || 0) + '</td>';
5516 html += '<td>' + (feature.avg_response_ms ? feature.avg_response_ms + ' ms' : '-') + '</td>';
5517 html += '<td><div class="wpforo-ai-success-rate"><span>' + successRate.toFixed(1) + '%</span><div class="wpforo-ai-success-rate-bar"><div class="wpforo-ai-success-rate-fill ' + rateClass + '" style="width: ' + successRate + '%"></div></div></div></td>';
5518 html += '</tr>';
5519 });
5520
5521 tbody.html(html);
5522 },
5523
5524 hideLoading: function() {
5525 $('.wpforo-ai-chart-loading').hide();
5526 $('#wpforo-ai-analytics-main-loading').addClass('hidden');
5527 $('#wpforo-ai-analytics-content').addClass('loaded');
5528 },
5529
5530 showNoData: function(container) {
5531 $(container).html('<div class="wpforo-ai-analytics-empty"><span class="dashicons dashicons-chart-bar"></span><h3>' + wpforoAIAnalytics.i18n.noData + '</h3></div>');
5532 },
5533
5534 showError: function(message) {
5535 $('#wpforo-ai-analytics-main-loading').html('<span class="dashicons dashicons-warning" style="color:#d63638;font-size:24px;"></span><span style="color:#d63638;">' + message + '</span>');
5536 $('.wpforo-ai-chart-loading').html('<div class="wpforo-ai-analytics-error"><span class="dashicons dashicons-warning"></span><p>' + message + '</p></div>');
5537 $('#feature-usage-tbody').html('<tr class="wpforo-ai-no-data"><td colspan="5">' + message + '</td></tr>');
5538 $('.wpforo-ai-analytics-card-value').text('-');
5539 },
5540
5541 formatNumber: function(num, decimals) {
5542 decimals = decimals || 0;
5543 return parseFloat(num).toLocaleString(undefined, {
5544 minimumFractionDigits: decimals,
5545 maximumFractionDigits: decimals
5546 });
5547 }
5548 };
5549
5550 // Initialize Analytics if on that tab
5551 if ($('.wpforo-ai-analytics-tab').length) {
5552 WpForoAIAnalytics.init();
5553 }
5554
5555 // Make available globally
5556 window.WpForoAIAnalytics = WpForoAIAnalytics;
5557
5558 // ===== Forum Activity Analytics =====
5559 const WpForoForumActivity = {
5560 charts: {},
5561
5562 init: function() {
5563 if (typeof wpforoForumActivity === 'undefined') {
5564 return;
5565 }
5566
5567 this.initPostsOverTimeChart();
5568 this.initTopForumsChart();
5569 },
5570
5571 destroyCharts: function() {
5572 Object.keys(this.charts).forEach(function(key) {
5573 if (this.charts[key]) {
5574 this.charts[key].destroy();
5575 this.charts[key] = null;
5576 }
5577 }.bind(this));
5578 },
5579
5580 initPostsOverTimeChart: function() {
5581 const canvas = document.getElementById('forum-posts-chart');
5582 if (!canvas) return;
5583
5584 // Destroy existing chart
5585 const existingChart = Chart.getChart(canvas);
5586 if (existingChart) {
5587 existingChart.destroy();
5588 }
5589
5590 const data = wpforoForumActivity.postsOverTime || [];
5591 const showYear = data.length > 1 && new Date(data[0].date).getFullYear() !== new Date(data[data.length - 1].date).getFullYear();
5592 const labels = data.map(function(d) {
5593 const date = new Date(d.date);
5594 const opts = showYear ? { month: 'short', day: 'numeric', year: '2-digit' } : { month: 'short', day: 'numeric' };
5595 return date.toLocaleDateString(undefined, opts);
5596 });
5597 const topics = data.map(function(d) { return d.topics || 0; });
5598 const replies = data.map(function(d) { return d.replies || 0; });
5599
5600 this.charts.postsOverTime = new Chart(canvas, {
5601 type: 'line',
5602 data: {
5603 labels: labels,
5604 datasets: [
5605 {
5606 label: wpforoForumActivity.i18n.topics || 'Topics',
5607 data: topics,
5608 borderColor: '#4CAF50',
5609 backgroundColor: 'rgba(76, 175, 80, 0.1)',
5610 fill: true,
5611 tension: 0.3,
5612 borderWidth: 2,
5613 pointRadius: 3,
5614 pointHoverRadius: 5
5615 },
5616 {
5617 label: wpforoForumActivity.i18n.replies || 'Replies',
5618 data: replies,
5619 borderColor: '#2196F3',
5620 backgroundColor: 'rgba(33, 150, 243, 0.1)',
5621 fill: true,
5622 tension: 0.3,
5623 borderWidth: 2,
5624 pointRadius: 3,
5625 pointHoverRadius: 5
5626 }
5627 ]
5628 },
5629 options: {
5630 responsive: true,
5631 maintainAspectRatio: false,
5632 interaction: {
5633 intersect: false,
5634 mode: 'index'
5635 },
5636 plugins: {
5637 legend: {
5638 position: 'top',
5639 labels: {
5640 usePointStyle: true,
5641 padding: 15
5642 }
5643 },
5644 tooltip: {
5645 callbacks: {
5646 label: function(context) {
5647 return context.dataset.label + ': ' + context.parsed.y.toLocaleString();
5648 }
5649 }
5650 }
5651 },
5652 scales: {
5653 y: {
5654 beginAtZero: true,
5655 ticks: {
5656 precision: 0
5657 }
5658 }
5659 }
5660 }
5661 });
5662 },
5663
5664 initTopForumsChart: function() {
5665 const canvas = document.getElementById('top-forums-chart');
5666 if (!canvas) return;
5667
5668 // Destroy existing chart
5669 const existingChart = Chart.getChart(canvas);
5670 if (existingChart) {
5671 existingChart.destroy();
5672 }
5673
5674 const forums = wpforoForumActivity.topForums || [];
5675 if (forums.length === 0) {
5676 $(canvas).parent().html('<div class="wpforo-ai-analytics-empty"><span class="dashicons dashicons-chart-bar"></span><p>No forum activity in this period</p></div>');
5677 return;
5678 }
5679
5680 const labels = forums.map(function(f) {
5681 // Truncate long forum names
5682 const title = f.title || 'Unknown';
5683 return title.length > 20 ? title.substring(0, 18) + '...' : title;
5684 });
5685 const topicsData = forums.map(function(f) { return parseInt(f.topics) || 0; });
5686 const repliesData = forums.map(function(f) { return parseInt(f.replies) || 0; });
5687
5688 this.charts.topForums = new Chart(canvas, {
5689 type: 'bar',
5690 data: {
5691 labels: labels,
5692 datasets: [
5693 {
5694 label: wpforoForumActivity.i18n.topics || 'Topics',
5695 data: topicsData,
5696 backgroundColor: 'rgba(76, 175, 80, 0.8)',
5697 borderColor: '#4CAF50',
5698 borderWidth: 1
5699 },
5700 {
5701 label: wpforoForumActivity.i18n.replies || 'Replies',
5702 data: repliesData,
5703 backgroundColor: 'rgba(33, 150, 243, 0.8)',
5704 borderColor: '#2196F3',
5705 borderWidth: 1
5706 }
5707 ]
5708 },
5709 options: {
5710 responsive: true,
5711 maintainAspectRatio: false,
5712 indexAxis: 'y',
5713 plugins: {
5714 legend: {
5715 position: 'top',
5716 labels: {
5717 usePointStyle: true,
5718 padding: 10
5719 }
5720 },
5721 tooltip: {
5722 callbacks: {
5723 title: function(context) {
5724 // Show full forum name in tooltip
5725 const idx = context[0].dataIndex;
5726 return forums[idx].title || 'Unknown';
5727 }
5728 }
5729 }
5730 },
5731 scales: {
5732 x: {
5733 beginAtZero: true,
5734 stacked: true,
5735 ticks: {
5736 precision: 0
5737 }
5738 },
5739 y: {
5740 stacked: true
5741 }
5742 }
5743 }
5744 });
5745 }
5746 };
5747
5748 // Initialize Forum Activity if on that sub-tab
5749 if (typeof wpforoForumActivity !== 'undefined') {
5750 WpForoForumActivity.init();
5751 }
5752
5753 window.WpForoForumActivity = WpForoForumActivity;
5754
5755 // ===== User Engagement Analytics =====
5756 const WpForoUserEngagement = {
5757 charts: {},
5758
5759 init: function() {
5760 if (typeof wpforoUserEngagement === 'undefined') {
5761 return;
5762 }
5763
5764 this.initRegistrationsChart();
5765 this.initDistributionChart();
5766 },
5767
5768 destroyCharts: function() {
5769 Object.keys(this.charts).forEach(function(key) {
5770 if (this.charts[key]) {
5771 this.charts[key].destroy();
5772 this.charts[key] = null;
5773 }
5774 }.bind(this));
5775 },
5776
5777 initRegistrationsChart: function() {
5778 const canvas = document.getElementById('registrations-chart');
5779 if (!canvas) return;
5780
5781 // Destroy existing chart
5782 const existingChart = Chart.getChart(canvas);
5783 if (existingChart) {
5784 existingChart.destroy();
5785 }
5786
5787 const data = wpforoUserEngagement.registrations || [];
5788 const showYear = data.length > 1 && new Date(data[0].date).getFullYear() !== new Date(data[data.length - 1].date).getFullYear();
5789 const labels = data.map(function(d) {
5790 const date = new Date(d.date);
5791 const opts = showYear ? { month: 'short', day: 'numeric', year: '2-digit' } : { month: 'short', day: 'numeric' };
5792 return date.toLocaleDateString(undefined, opts);
5793 });
5794 const counts = data.map(function(d) { return d.count || 0; });
5795
5796 this.charts.registrations = new Chart(canvas, {
5797 type: 'line',
5798 data: {
5799 labels: labels,
5800 datasets: [{
5801 label: wpforoUserEngagement.i18n.newRegistrations || 'New Registrations',
5802 data: counts,
5803 borderColor: '#9C27B0',
5804 backgroundColor: 'rgba(156, 39, 176, 0.1)',
5805 fill: true,
5806 tension: 0.3,
5807 borderWidth: 2,
5808 pointRadius: 3,
5809 pointHoverRadius: 5
5810 }]
5811 },
5812 options: {
5813 responsive: true,
5814 maintainAspectRatio: false,
5815 interaction: {
5816 intersect: false,
5817 mode: 'index'
5818 },
5819 plugins: {
5820 legend: {
5821 position: 'top',
5822 labels: {
5823 usePointStyle: true,
5824 padding: 15
5825 }
5826 },
5827 tooltip: {
5828 callbacks: {
5829 label: function(context) {
5830 return context.dataset.label + ': ' + context.parsed.y.toLocaleString();
5831 }
5832 }
5833 }
5834 },
5835 scales: {
5836 y: {
5837 beginAtZero: true,
5838 ticks: {
5839 precision: 0
5840 }
5841 }
5842 }
5843 }
5844 });
5845 },
5846
5847 initDistributionChart: function() {
5848 const canvas = document.getElementById('user-distribution-chart');
5849 if (!canvas) return;
5850
5851 // Destroy existing chart
5852 const existingChart = Chart.getChart(canvas);
5853 if (existingChart) {
5854 existingChart.destroy();
5855 }
5856
5857 const dist = wpforoUserEngagement.distribution || {};
5858 const i18n = wpforoUserEngagement.i18n || {};
5859
5860 const labels = [
5861 i18n.powerUsers || 'Power Users (50+)',
5862 i18n.activeUsers || 'Active (10-49)',
5863 i18n.occasional || 'Occasional (2-9)',
5864 i18n.oneTime || 'One-time (1)',
5865 i18n.lurkers || 'Lurkers (0)'
5866 ];
5867
5868 const data = [
5869 dist.power_users || 0,
5870 dist.active_users || 0,
5871 dist.occasional || 0,
5872 dist.one_time || 0,
5873 dist.lurkers || 0
5874 ];
5875
5876 const colors = [
5877 '#4CAF50', // Green - Power users
5878 '#2196F3', // Blue - Active
5879 '#FF9800', // Orange - Occasional
5880 '#9C27B0', // Purple - One-time
5881 '#9E9E9E' // Gray - Lurkers
5882 ];
5883
5884 this.charts.distribution = new Chart(canvas, {
5885 type: 'doughnut',
5886 data: {
5887 labels: labels,
5888 datasets: [{
5889 data: data,
5890 backgroundColor: colors,
5891 borderWidth: 0,
5892 hoverOffset: 4
5893 }]
5894 },
5895 options: {
5896 responsive: true,
5897 maintainAspectRatio: false,
5898 cutout: '60%',
5899 plugins: {
5900 legend: {
5901 position: 'right',
5902 labels: {
5903 usePointStyle: true,
5904 padding: 12,
5905 generateLabels: function(chart) {
5906 const datasets = chart.data.datasets;
5907 return chart.data.labels.map(function(label, i) {
5908 const value = datasets[0].data[i];
5909 return {
5910 text: label + ': ' + value.toLocaleString(),
5911 fillStyle: colors[i],
5912 strokeStyle: colors[i],
5913 lineWidth: 0,
5914 pointStyle: 'circle',
5915 hidden: false,
5916 index: i
5917 };
5918 });
5919 }
5920 }
5921 },
5922 tooltip: {
5923 callbacks: {
5924 label: function(context) {
5925 const total = context.dataset.data.reduce(function(a, b) { return a + b; }, 0);
5926 const value = context.raw;
5927 const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
5928 return context.label + ': ' + value.toLocaleString() + ' (' + percentage + '%)';
5929 }
5930 }
5931 }
5932 }
5933 }
5934 });
5935 }
5936 };
5937
5938 // Initialize User Engagement if on that sub-tab
5939 if (typeof wpforoUserEngagement !== 'undefined') {
5940 WpForoUserEngagement.init();
5941 }
5942
5943 window.WpForoUserEngagement = WpForoUserEngagement;
5944
5945 /* ==========================================================================
5946 Content Performance Analytics Module
5947 ========================================================================== */
5948
5949 var WpForoContentPerformance = {
5950 charts: {},
5951
5952 init: function() {
5953 this.initForumDistributionChart();
5954 },
5955
5956 initForumDistributionChart: function() {
5957 const canvas = document.getElementById('content-distribution-chart');
5958 if (!canvas) return;
5959
5960 // Destroy existing chart
5961 const existingChart = Chart.getChart(canvas);
5962 if (existingChart) {
5963 existingChart.destroy();
5964 }
5965
5966 const distribution = wpforoContentPerformance.forumDistribution || [];
5967 const i18n = wpforoContentPerformance.i18n || {};
5968
5969 if (distribution.length === 0) {
5970 return;
5971 }
5972
5973 const labels = distribution.map(function(item) {
5974 return item.title;
5975 });
5976
5977 const data = distribution.map(function(item) {
5978 return parseInt(item.topics, 10);
5979 });
5980
5981 // Generate colors for each forum
5982 const colors = this.generateColors(distribution.length);
5983
5984 this.charts.forumDistribution = new Chart(canvas, {
5985 type: 'pie',
5986 data: {
5987 labels: labels,
5988 datasets: [{
5989 data: data,
5990 backgroundColor: colors,
5991 borderWidth: 2,
5992 borderColor: '#fff',
5993 hoverOffset: 8
5994 }]
5995 },
5996 options: {
5997 responsive: true,
5998 maintainAspectRatio: false,
5999 plugins: {
6000 legend: {
6001 position: 'right',
6002 labels: {
6003 usePointStyle: true,
6004 padding: 12,
6005 font: {
6006 size: 12
6007 },
6008 generateLabels: function(chart) {
6009 const datasets = chart.data.datasets;
6010 const total = datasets[0].data.reduce(function(a, b) { return a + b; }, 0);
6011 return chart.data.labels.map(function(label, i) {
6012 const value = datasets[0].data[i];
6013 const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
6014 return {
6015 text: label + ': ' + value.toLocaleString() + ' (' + percentage + '%)',
6016 fillStyle: colors[i],
6017 strokeStyle: '#fff',
6018 lineWidth: 1,
6019 pointStyle: 'circle',
6020 hidden: false,
6021 index: i
6022 };
6023 });
6024 }
6025 }
6026 },
6027 tooltip: {
6028 callbacks: {
6029 label: function(context) {
6030 const total = context.dataset.data.reduce(function(a, b) { return a + b; }, 0);
6031 const value = context.raw;
6032 const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
6033 const topicsLabel = i18n.topics || 'Topics';
6034 return context.label + ': ' + value.toLocaleString() + ' ' + topicsLabel + ' (' + percentage + '%)';
6035 }
6036 }
6037 }
6038 }
6039 }
6040 });
6041 },
6042
6043 generateColors: function(count) {
6044 // Predefined colors for forums
6045 const baseColors = [
6046 '#2196F3', // Blue
6047 '#4CAF50', // Green
6048 '#FF9800', // Orange
6049 '#9C27B0', // Purple
6050 '#F44336', // Red
6051 '#00BCD4', // Cyan
6052 '#795548', // Brown
6053 '#607D8B', // Blue Grey
6054 '#E91E63', // Pink
6055 '#3F51B5', // Indigo
6056 '#009688', // Teal
6057 '#CDDC39', // Lime
6058 '#FFC107', // Amber
6059 '#673AB7', // Deep Purple
6060 '#8BC34A' // Light Green
6061 ];
6062
6063 const colors = [];
6064 for (var i = 0; i < count; i++) {
6065 colors.push(baseColors[i % baseColors.length]);
6066 }
6067 return colors;
6068 }
6069 };
6070
6071 // Initialize Content Performance if on that sub-tab
6072 if (typeof wpforoContentPerformance !== 'undefined') {
6073 WpForoContentPerformance.init();
6074 }
6075
6076 window.WpForoContentPerformance = WpForoContentPerformance;
6077
6078 /* ==========================================================================
6079 AI Insights Module
6080 ========================================================================== */
6081
6082 var WpForoAIInsights = {
6083 config: null,
6084 activeModal: null,
6085
6086 init: function() {
6087 if (typeof wpforoAIInsights === 'undefined') {
6088 return;
6089 }
6090 this.config = wpforoAIInsights;
6091 this.bindEvents();
6092 },
6093
6094 bindEvents: function() {
6095 var self = this;
6096
6097 // Run Insight buttons
6098 $(document).off('click.aiInsights', '.wpforo-ai-run-insight-btn').on('click.aiInsights', '.wpforo-ai-run-insight-btn', function(e) {
6099 e.preventDefault();
6100 var $btn = $(this);
6101 var insightType = $btn.data('insight-type');
6102 var credits = parseInt($btn.data('credits'), 10);
6103
6104 if ($btn.prop('disabled')) {
6105 return;
6106 }
6107
6108 self.showConfirmModal(insightType, credits);
6109 });
6110 },
6111
6112 showConfirmModal: function(insightType, credits) {
6113 var self = this;
6114 var i18n = this.config.i18n;
6115
6116 // Create modal HTML
6117 var modalHtml = '<div class="wpforo-ai-insights-modal-overlay">' +
6118 '<div class="wpforo-ai-insights-modal">' +
6119 '<div class="wpforo-ai-insights-modal-header">' +
6120 '<h3>' + i18n.confirmTitle + '</h3>' +
6121 '</div>' +
6122 '<div class="wpforo-ai-insights-modal-body">' +
6123 '<p>' + i18n.confirmMessage.replace('%d', credits) + '</p>' +
6124 '</div>' +
6125 '<div class="wpforo-ai-insights-modal-footer">' +
6126 '<button type="button" class="button wpforo-ai-insights-cancel-btn">' + i18n.cancelButton + '</button>' +
6127 '<button type="button" class="button button-primary wpforo-ai-insights-confirm-btn">' + i18n.confirmButton + '</button>' +
6128 '</div>' +
6129 '</div>' +
6130 '</div>';
6131
6132 // Remove any existing modal
6133 this.closeModal();
6134
6135 // Add modal to body
6136 $('body').append(modalHtml);
6137 this.activeModal = $('.wpforo-ai-insights-modal-overlay');
6138
6139 // Bind modal events
6140 this.activeModal.find('.wpforo-ai-insights-cancel-btn').on('click', function() {
6141 self.closeModal();
6142 });
6143
6144 this.activeModal.find('.wpforo-ai-insights-confirm-btn').on('click', function() {
6145 self.closeModal();
6146 self.runInsight(insightType);
6147 });
6148
6149 // Close on overlay click
6150 this.activeModal.on('click', function(e) {
6151 if ($(e.target).hasClass('wpforo-ai-insights-modal-overlay')) {
6152 self.closeModal();
6153 }
6154 });
6155
6156 // Close on escape key
6157 $(document).on('keydown.aiInsightsModal', function(e) {
6158 if (e.key === 'Escape') {
6159 self.closeModal();
6160 }
6161 });
6162 },
6163
6164 closeModal: function() {
6165 if (this.activeModal) {
6166 this.activeModal.remove();
6167 this.activeModal = null;
6168 }
6169 $(document).off('keydown.aiInsightsModal');
6170 },
6171
6172 runInsight: function(insightType) {
6173 var self = this;
6174 var $widget = $('.wpforo-ai-insights-widget[data-insight-type="' + insightType + '"]');
6175 var $btn = $widget.find('.wpforo-ai-run-insight-btn');
6176 var $loading = $widget.find('.wpforo-ai-insights-loading');
6177 var $results = $widget.find('.wpforo-ai-insights-results');
6178 var $error = $widget.find('.wpforo-ai-insights-error');
6179
6180 // Show loading state
6181 $btn.prop('disabled', true);
6182 $loading.show();
6183 $results.hide();
6184 $error.hide();
6185
6186 // Make AJAX request
6187 $.ajax({
6188 url: this.config.ajaxUrl,
6189 type: 'POST',
6190 data: {
6191 action: 'wpforo_ai_run_insight',
6192 nonce: this.config.nonce,
6193 insight_type: insightType,
6194 board_id: this.config.boardId
6195 },
6196 success: function(response) {
6197 $loading.hide();
6198 $btn.prop('disabled', false);
6199
6200 if (response.success) {
6201 // Update results and add "Just now" cached notice
6202 var cachedNotice = '<div class="wpforo-ai-insights-cached-notice"><span class="dashicons dashicons-clock"></span> ' + (self.config.i18n.cachedJustNow || 'Just now') + '</div>';
6203 $results.html(response.data.html + cachedNotice).show();
6204
6205 // Remove outdated notice since we just refreshed the data
6206 $widget.find('.wpforo-ai-insights-outdated-notice').remove();
6207
6208 // Hide outdated notice since we just refreshed the data
6209 $widget.find('.wpforo-ai-insights-outdated-notice').hide();
6210
6211 // Update button to "Refresh" state
6212 if (!$btn.hasClass('has-results')) {
6213 $btn.html('<span class="dashicons dashicons-update"></span> Refresh Analysis');
6214 $btn.addClass('has-results');
6215 }
6216
6217 // Update credits remaining
6218 if (response.data.credits_remaining !== undefined) {
6219 self.config.creditsRemaining = response.data.credits_remaining;
6220 $('.wpforo-ai-insights-credits-number').text(self.formatNumber(response.data.credits_remaining));
6221 self.updateButtonStates();
6222 }
6223 } else {
6224 $error.text(response.data.message || self.config.i18n.error).show();
6225 }
6226 },
6227 error: function() {
6228 $loading.hide();
6229 $btn.prop('disabled', false);
6230 $error.text(self.config.i18n.error).show();
6231 }
6232 });
6233 },
6234
6235 updateButtonStates: function() {
6236 var self = this;
6237 $('.wpforo-ai-run-insight-btn').each(function() {
6238 var $btn = $(this);
6239 var credits = parseInt($btn.data('credits'), 10);
6240 var $insufficient = $btn.siblings('.wpforo-ai-insights-insufficient');
6241
6242 if (self.config.creditsRemaining < credits) {
6243 $btn.prop('disabled', true);
6244 if ($insufficient.length === 0) {
6245 $btn.after('<span class="wpforo-ai-insights-insufficient">' + self.config.i18n.insufficientCredits + '</span>');
6246 }
6247 } else {
6248 $btn.prop('disabled', false);
6249 $insufficient.remove();
6250 }
6251 });
6252 },
6253
6254 formatNumber: function(num) {
6255 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
6256 }
6257 };
6258
6259 // Initialize AI Insights if on that sub-tab
6260 if (typeof wpforoAIInsights !== 'undefined') {
6261 WpForoAIInsights.init();
6262 }
6263
6264 window.WpForoAIInsights = WpForoAIInsights;
6265
6266 /**
6267 * AI Logs Tab Manager
6268 * Handles filtering, pagination, detail view, and bulk operations for AI logs
6269 */
6270 const WpForoAILogs = {
6271 config: {
6272 perPage: 50,
6273 currentPage: 1,
6274 totalLogs: 0,
6275 viewMode: 'logs', // 'logs' or 'chat_messages'
6276 filters: {
6277 action_type: '',
6278 date_range: 'all',
6279 status: '',
6280 user_type: '',
6281 search: ''
6282 }
6283 },
6284
6285 init: function() {
6286 if (!$('#wpforo-ai-logs-tab').length) {
6287 return;
6288 }
6289
6290 this.cacheElements();
6291 this.bindEvents();
6292 this.updateShowingText();
6293 },
6294
6295 cacheElements: function() {
6296 this.$container = $('#wpforo-ai-logs-tab');
6297 this.$nonce = this.$container.data('nonce');
6298 this.$boardid = this.$container.data('boardid') || 0;
6299 this.$table = $('#wpforo-ai-logs-table');
6300 this.$tbody = $('#wpforo-ai-logs-tbody');
6301 this.$loading = $('#wpforo-ai-logs-loading');
6302 this.$pagination = $('#wpforo-ai-logs-pagination');
6303 this.$totalCount = $('#wpforo-ai-logs-total-count');
6304 this.$showing = $('#wpforo-ai-logs-showing');
6305 this.$detailOverlay = $('#wpforo-ai-log-detail-overlay');
6306 this.$detailBody = $('#wpforo-ai-log-detail-body');
6307 this.$emptyConfirmOverlay = $('#wpforo-ai-empty-confirm-overlay');
6308
6309 // Read per page from data attribute
6310 var perPageData = this.$pagination.data('per-page');
6311 if (perPageData) {
6312 this.config.perPage = parseInt(perPageData, 10) || 50;
6313 }
6314 },
6315
6316 bindEvents: function() {
6317 var self = this;
6318
6319 // Unbind all log events first to prevent duplicates
6320 $(document).off('change', '.wpforo-ai-logs-filter');
6321 $(document).off('keypress', '#wpforo-ai-logs-search');
6322 $(document).off('click', '#wpforo-ai-logs-apply-filter');
6323 $(document).off('click', '#wpforo-ai-logs-reset-filter');
6324 $(document).off('click', '#wpforo-ai-logs-chat-messages-btn');
6325 $(document).off('click', '.wpforo-ai-logs-pagination .button');
6326 $(document).off('change', '#wpforo-ai-logs-select-all');
6327 $(document).off('click', '#wpforo-ai-logs-bulk-apply');
6328 $(document).off('click', '#wpforo-ai-empty-logs-btn');
6329 $(document).off('click', '#wpforo-ai-confirm-empty-logs');
6330 $(document).off('click', '#wpforo-ai-cancel-empty-logs');
6331 $(document).off('click', '.wpforo-ai-confirm-overlay');
6332 $(document).off('blur change', '#wpforo-ai-logs-cleanup-days');
6333 $(document).off('change', '#wpforo-ai-logs-per-page');
6334 $(document).off('click', '.wpforo-ai-log-view');
6335 $(document).off('click', '.wpforo-ai-log-delete');
6336 $(document).off('click', '.wpforo-ai-chat-message-view');
6337 $(document).off('click', '.wpforo-ai-log-detail-close');
6338 $(document).off('click', '.wpforo-ai-log-detail-overlay');
6339
6340 // Filter controls
6341 $(document).on('change', '.wpforo-ai-logs-filter', function() {
6342 // Auto-apply on change for selects
6343 if ($(this).is('select')) {
6344 self.applyFilters();
6345 }
6346 });
6347
6348 $(document).on('keypress', '#wpforo-ai-logs-search', function(e) {
6349 if (e.which === 13) {
6350 e.preventDefault();
6351 self.applyFilters();
6352 }
6353 });
6354
6355 $(document).on('click', '#wpforo-ai-logs-apply-filter', function(e) {
6356 e.preventDefault();
6357 self.applyFilters();
6358 });
6359
6360 $(document).on('click', '#wpforo-ai-logs-reset-filter', function(e) {
6361 e.preventDefault();
6362 self.resetFilters();
6363 });
6364
6365 // AI ChatBot Messages button
6366 $(document).on('click', '#wpforo-ai-logs-chat-messages-btn', function(e) {
6367 e.preventDefault();
6368 self.showChatMessages();
6369 });
6370
6371 // View chat message detail
6372 $(document).on('click', '.wpforo-ai-chat-message-view', function(e) {
6373 e.preventDefault();
6374 var messageId = $(this).data('message-id');
6375 self.showChatMessageDetail(messageId);
6376 });
6377
6378 // Pagination
6379 $(document).on('click', '.wpforo-ai-logs-pagination .button', function(e) {
6380 e.preventDefault();
6381 if (!$(this).prop('disabled')) {
6382 var page = $(this).data('page');
6383 self.goToPage(page);
6384 }
6385 });
6386
6387 // Select all checkbox
6388 $(document).on('change', '#wpforo-ai-logs-select-all', function() {
6389 $('.wpforo-ai-log-checkbox').prop('checked', $(this).prop('checked'));
6390 });
6391
6392 // Bulk action
6393 $(document).on('click', '#wpforo-ai-logs-bulk-apply', function(e) {
6394 e.preventDefault();
6395 self.applyBulkAction();
6396 });
6397
6398 // View log detail
6399 $(document).on('click', '.wpforo-ai-log-view', function(e) {
6400 e.preventDefault();
6401 var logId = $(this).data('log-id');
6402 self.showLogDetail(logId);
6403 });
6404
6405 // Delete single log
6406 $(document).on('click', '.wpforo-ai-log-delete', function(e) {
6407 e.preventDefault();
6408 var logId = $(this).data('log-id');
6409 if (confirm(wpforoAI.i18n.confirmDelete || 'Are you sure you want to delete this log?')) {
6410 self.deleteLogs([logId]);
6411 }
6412 });
6413
6414 // Close detail modal
6415 $(document).on('click', '#wpforo-ai-log-detail-close', function(e) {
6416 e.preventDefault();
6417 self.$detailOverlay.hide();
6418 });
6419
6420 $(document).on('click', '.wpforo-ai-log-detail-overlay', function(e) {
6421 if ($(e.target).hasClass('wpforo-ai-log-detail-overlay')) {
6422 self.$detailOverlay.hide();
6423 }
6424 });
6425
6426 // Empty all logs
6427 $(document).on('click', '#wpforo-ai-empty-logs-btn', function(e) {
6428 e.preventDefault();
6429 self.$emptyConfirmOverlay.show();
6430 });
6431
6432 $(document).on('click', '#wpforo-ai-empty-cancel', function(e) {
6433 e.preventDefault();
6434 self.$emptyConfirmOverlay.hide();
6435 });
6436
6437 $(document).on('click', '#wpforo-ai-empty-confirm', function(e) {
6438 e.preventDefault();
6439 self.emptyAllLogs();
6440 });
6441
6442 $(document).on('click', '.wpforo-ai-confirm-overlay', function(e) {
6443 if ($(e.target).hasClass('wpforo-ai-confirm-overlay')) {
6444 self.$emptyConfirmOverlay.hide();
6445 }
6446 });
6447
6448 // Save cleanup days setting on blur or change (arrows trigger change)
6449 var cleanupDaysOriginal = $('#wpforo-ai-logs-cleanup-days').val();
6450 $(document).on('blur change', '#wpforo-ai-logs-cleanup-days', function() {
6451 var $input = $(this);
6452 var $spinner = $('#wpforo-ai-logs-cleanup-spinner');
6453 var $saved = $('#wpforo-ai-logs-cleanup-saved');
6454 var days = parseInt($input.val(), 10) || 0;
6455
6456 // Only save if value changed
6457 if (days.toString() === cleanupDaysOriginal) {
6458 return;
6459 }
6460
6461 $input.prop('disabled', true);
6462 $spinner.addClass('is-active');
6463 $saved.removeClass('is-visible');
6464
6465 $.ajax({
6466 url: ajaxurl,
6467 type: 'POST',
6468 data: {
6469 action: 'wpforo_ai_save_cleanup_days',
6470 nonce: self.$nonce,
6471 boardid: self.$boardid,
6472 days: days
6473 },
6474 success: function(response) {
6475 $input.prop('disabled', false);
6476 $spinner.removeClass('is-active');
6477 if (response.success) {
6478 cleanupDaysOriginal = days.toString();
6479 $saved.addClass('is-visible');
6480 setTimeout(function() { $saved.removeClass('is-visible'); }, 2000);
6481 }
6482 },
6483 error: function() {
6484 $input.prop('disabled', false);
6485 $spinner.removeClass('is-active');
6486 }
6487 });
6488 });
6489
6490 // Save per page setting on change
6491 $(document).on('change', '#wpforo-ai-logs-per-page', function() {
6492 var $select = $(this);
6493 var $spinner = $('#wpforo-ai-logs-per-page-spinner');
6494 var $saved = $('#wpforo-ai-logs-per-page-saved');
6495 var perPage = parseInt($select.val(), 10) || 50;
6496
6497 $select.prop('disabled', true);
6498 $spinner.addClass('is-active');
6499 $saved.removeClass('is-visible');
6500
6501 $.ajax({
6502 url: ajaxurl,
6503 type: 'POST',
6504 data: {
6505 action: 'wpforo_ai_save_per_page',
6506 nonce: self.$nonce,
6507 boardid: self.$boardid,
6508 per_page: perPage
6509 },
6510 success: function(response) {
6511 $select.prop('disabled', false);
6512 $spinner.removeClass('is-active');
6513 if (response.success) {
6514 self.config.perPage = perPage;
6515 self.config.currentPage = 1;
6516 $saved.addClass('is-visible');
6517 setTimeout(function() { $saved.removeClass('is-visible'); }, 2000);
6518 self.loadLogs();
6519 }
6520 },
6521 error: function() {
6522 $select.prop('disabled', false);
6523 $spinner.removeClass('is-active');
6524 }
6525 });
6526 });
6527
6528 // ESC key to close modals
6529 $(document).on('keyup', function(e) {
6530 if (e.key === 'Escape') {
6531 self.$detailOverlay.hide();
6532 self.$emptyConfirmOverlay.hide();
6533 }
6534 });
6535 },
6536
6537 applyFilters: function() {
6538 this.config.filters.action_type = $('#wpforo-ai-logs-filter-action').val();
6539 this.config.filters.date_range = $('#wpforo-ai-logs-filter-date').val();
6540 this.config.filters.status = $('#wpforo-ai-logs-filter-status').val();
6541 this.config.filters.user_type = $('#wpforo-ai-logs-filter-user-type').val();
6542 this.config.filters.search = $('#wpforo-ai-logs-search').val();
6543 this.config.currentPage = 1;
6544
6545 if (this.config.viewMode === 'chat_messages') {
6546 this.loadChatMessages();
6547 } else {
6548 this.loadLogs();
6549 }
6550 },
6551
6552 resetFilters: function() {
6553 $('#wpforo-ai-logs-filter-action').val('');
6554 $('#wpforo-ai-logs-filter-date').val('all');
6555 $('#wpforo-ai-logs-filter-status').val('');
6556 $('#wpforo-ai-logs-filter-user-type').val('');
6557 $('#wpforo-ai-logs-search').val('');
6558 this.config.filters = {
6559 action_type: '',
6560 date_range: 'all',
6561 status: '',
6562 user_type: '',
6563 search: ''
6564 };
6565 this.config.currentPage = 1;
6566
6567 // Always switch back to logs mode on reset
6568 if (this.config.viewMode === 'chat_messages') {
6569 this.config.viewMode = 'logs';
6570 $('#wpforo-ai-logs-chat-messages-btn').removeClass('active');
6571 $('#wpforo-ai-logs-filter-action').prop('disabled', false);
6572 }
6573
6574 this.loadLogs();
6575 },
6576
6577 showChatMessages: function() {
6578 // Update filters from current values (except action type)
6579 this.config.filters.date_range = $('#wpforo-ai-logs-filter-date').val();
6580 this.config.filters.status = $('#wpforo-ai-logs-filter-status').val();
6581 this.config.filters.user_type = $('#wpforo-ai-logs-filter-user-type').val();
6582 this.config.filters.search = $('#wpforo-ai-logs-search').val();
6583 this.config.currentPage = 1;
6584 this.config.viewMode = 'chat_messages';
6585
6586 // Disable action type filter and highlight button
6587 $('#wpforo-ai-logs-filter-action').prop('disabled', true);
6588 $('#wpforo-ai-logs-chat-messages-btn').addClass('active');
6589
6590 this.loadChatMessages();
6591 },
6592
6593 loadChatMessages: function() {
6594 var self = this;
6595
6596 self.$loading.show();
6597 self.$tbody.css('opacity', '0.5');
6598
6599 $.ajax({
6600 url: ajaxurl,
6601 type: 'POST',
6602 data: {
6603 action: 'wpforo_ai_get_chat_messages',
6604 nonce: self.$nonce,
6605 boardid: self.$boardid,
6606 page: self.config.currentPage,
6607 per_page: self.config.perPage,
6608 date_range: self.config.filters.date_range,
6609 status: self.config.filters.status,
6610 user_type: self.config.filters.user_type,
6611 search: self.config.filters.search
6612 },
6613 success: function(response) {
6614 self.$loading.hide();
6615 self.$tbody.css('opacity', '1');
6616
6617 if (response.success) {
6618 self.$tbody.html(response.data.html);
6619 self.config.totalLogs = response.data.total;
6620 self.updatePagination();
6621 self.updateShowingText();
6622 self.$totalCount.text('(' + self.formatNumber(response.data.total) + ')');
6623 $('#wpforo-ai-logs-select-all').prop('checked', false);
6624 } else {
6625 self.showNotice(response.data.message || 'Error loading chat messages', 'error');
6626 }
6627 },
6628 error: function() {
6629 self.$loading.hide();
6630 self.$tbody.css('opacity', '1');
6631 self.showNotice('Failed to load chat messages', 'error');
6632 }
6633 });
6634 },
6635
6636 showChatMessageDetail: function(messageId) {
6637 var self = this;
6638
6639 self.$detailBody.html('<div class="wpforo-ai-logs-loading"><span class="spinner is-active"></span> Loading...</div>');
6640 self.$detailOverlay.show();
6641
6642 $.ajax({
6643 url: ajaxurl,
6644 type: 'POST',
6645 data: {
6646 action: 'wpforo_ai_get_chat_message_detail',
6647 nonce: self.$nonce,
6648 boardid: self.$boardid,
6649 message_id: messageId
6650 },
6651 success: function(response) {
6652 if (response.success) {
6653 self.$detailBody.html(response.data.html);
6654 } else {
6655 self.$detailBody.html('<div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-warning"></span><p>' + (response.data.message || 'Error loading message details') + '</p></div>');
6656 }
6657 },
6658 error: function() {
6659 self.$detailBody.html('<div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-warning"></span><p>Failed to load message details</p></div>');
6660 }
6661 });
6662 },
6663
6664 goToPage: function(page) {
6665 this.config.currentPage = parseInt(page, 10);
6666 if (this.config.viewMode === 'chat_messages') {
6667 this.loadChatMessages();
6668 } else {
6669 this.loadLogs();
6670 }
6671 },
6672
6673 loadLogs: function() {
6674 var self = this;
6675
6676 self.$loading.show();
6677 self.$tbody.css('opacity', '0.5');
6678
6679 $.ajax({
6680 url: ajaxurl,
6681 type: 'POST',
6682 data: {
6683 action: 'wpforo_ai_get_logs',
6684 nonce: self.$nonce,
6685 boardid: self.$boardid,
6686 page: self.config.currentPage,
6687 per_page: self.config.perPage,
6688 action_type: self.config.filters.action_type,
6689 date_range: self.config.filters.date_range,
6690 status: self.config.filters.status,
6691 user_type: self.config.filters.user_type,
6692 search: self.config.filters.search
6693 },
6694 success: function(response) {
6695 self.$loading.hide();
6696 self.$tbody.css('opacity', '1');
6697
6698 if (response.success) {
6699 self.$tbody.html(response.data.html);
6700 self.config.totalLogs = response.data.total;
6701 self.updatePagination();
6702 self.updateShowingText();
6703 self.$totalCount.text('(' + self.formatNumber(response.data.total) + ')');
6704 $('#wpforo-ai-logs-select-all').prop('checked', false);
6705 } else {
6706 self.showNotice(response.data.message || 'Error loading logs', 'error');
6707 }
6708 },
6709 error: function() {
6710 self.$loading.hide();
6711 self.$tbody.css('opacity', '1');
6712 self.showNotice('Failed to load logs', 'error');
6713 }
6714 });
6715 },
6716
6717 updatePagination: function() {
6718 var totalPages = Math.ceil(this.config.totalLogs / this.config.perPage);
6719 var currentPage = this.config.currentPage;
6720
6721 if (totalPages <= 1) {
6722 this.$pagination.html('');
6723 return;
6724 }
6725
6726 var html = '<div class="tablenav-pages">';
6727 html += '<span class="displaying-num">' + this.formatNumber(this.config.totalLogs) + ' items</span>';
6728 html += '<span class="pagination-links">';
6729
6730 // First page
6731 html += '<button type="button" class="button first-page" data-page="1" ' + (currentPage === 1 ? 'disabled' : '') + '>';
6732 html += '<span aria-hidden="true">&laquo;</span></button>';
6733
6734 // Previous page
6735 html += '<button type="button" class="button prev-page" data-page="' + (currentPage - 1) + '" ' + (currentPage === 1 ? 'disabled' : '') + '>';
6736 html += '<span aria-hidden="true">&lsaquo;</span></button>';
6737
6738 // Page indicator
6739 html += '<span class="paging-input">';
6740 html += '<span class="current-page">' + currentPage + '</span> of ';
6741 html += '<span class="total-pages">' + totalPages + '</span>';
6742 html += '</span>';
6743
6744 // Next page
6745 html += '<button type="button" class="button next-page" data-page="' + (currentPage + 1) + '" ' + (currentPage >= totalPages ? 'disabled' : '') + '>';
6746 html += '<span aria-hidden="true">&rsaquo;</span></button>';
6747
6748 // Last page
6749 html += '<button type="button" class="button last-page" data-page="' + totalPages + '" ' + (currentPage >= totalPages ? 'disabled' : '') + '>';
6750 html += '<span aria-hidden="true">&raquo;</span></button>';
6751
6752 html += '</span></div>';
6753
6754 this.$pagination.html(html);
6755 },
6756
6757 updateShowingText: function() {
6758 var start = ((this.config.currentPage - 1) * this.config.perPage) + 1;
6759 var end = Math.min(this.config.currentPage * this.config.perPage, this.config.totalLogs);
6760
6761 if (this.config.totalLogs === 0) {
6762 this.$showing.text('');
6763 } else {
6764 this.$showing.text('(Showing ' + start + '-' + end + ' of ' + this.formatNumber(this.config.totalLogs) + ')');
6765 }
6766 },
6767
6768 applyBulkAction: function() {
6769 var action = $('#wpforo-ai-logs-bulk-action').val();
6770 if (!action) {
6771 return;
6772 }
6773
6774 var selectedIds = [];
6775 $('.wpforo-ai-log-checkbox:checked').each(function() {
6776 selectedIds.push($(this).val());
6777 });
6778
6779 if (selectedIds.length === 0) {
6780 this.showNotice('Please select at least one log', 'warning');
6781 return;
6782 }
6783
6784 if (action === 'delete') {
6785 if (confirm(wpforoAI.i18n.confirmDeleteSelected || 'Are you sure you want to delete the selected logs?')) {
6786 this.deleteLogs(selectedIds);
6787 }
6788 }
6789 },
6790
6791 deleteLogs: function(ids) {
6792 var self = this;
6793
6794 $.ajax({
6795 url: ajaxurl,
6796 type: 'POST',
6797 data: {
6798 action: 'wpforo_ai_delete_logs',
6799 nonce: self.$nonce,
6800 boardid: self.$boardid,
6801 log_ids: ids
6802 },
6803 success: function(response) {
6804 if (response.success) {
6805 self.showNotice(response.data.message || 'Logs deleted successfully', 'success');
6806 self.loadLogs();
6807 } else {
6808 self.showNotice(response.data.message || 'Error deleting logs', 'error');
6809 }
6810 },
6811 error: function() {
6812 self.showNotice('Failed to delete logs', 'error');
6813 }
6814 });
6815 },
6816
6817 emptyAllLogs: function() {
6818 var self = this;
6819
6820 $('#wpforo-ai-empty-confirm').prop('disabled', true).text('Deleting...');
6821
6822 $.ajax({
6823 url: ajaxurl,
6824 type: 'POST',
6825 data: {
6826 action: 'wpforo_ai_empty_all_logs',
6827 nonce: self.$nonce,
6828 boardid: self.$boardid
6829 },
6830 success: function(response) {
6831 $('#wpforo-ai-empty-confirm').prop('disabled', false).text(wpforoAI.i18n.deleteAllLogs || 'Delete All Logs');
6832 self.$emptyConfirmOverlay.hide();
6833
6834 if (response.success) {
6835 self.showNotice(response.data.message || 'All logs deleted successfully', 'success');
6836 self.config.totalLogs = 0;
6837 self.config.currentPage = 1;
6838 self.$tbody.html('<tr class="wpforo-ai-logs-empty-row"><td colspan="8"><div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-info-outline"></span>' + (wpforoAI.i18n.noLogs || 'No logs found.') + '</div></td></tr>');
6839 self.$totalCount.text('(0)');
6840 self.updatePagination();
6841 self.updateShowingText();
6842 } else {
6843 self.showNotice(response.data.message || 'Error deleting logs', 'error');
6844 }
6845 },
6846 error: function() {
6847 $('#wpforo-ai-empty-confirm').prop('disabled', false).text(wpforoAI.i18n.deleteAllLogs || 'Delete All Logs');
6848 self.$emptyConfirmOverlay.hide();
6849 self.showNotice('Failed to delete all logs', 'error');
6850 }
6851 });
6852 },
6853
6854 showLogDetail: function(logId) {
6855 var self = this;
6856
6857 self.$detailBody.html('<div class="wpforo-ai-logs-loading"><span class="spinner is-active"></span> Loading...</div>');
6858 self.$detailOverlay.show();
6859
6860 $.ajax({
6861 url: ajaxurl,
6862 type: 'POST',
6863 data: {
6864 action: 'wpforo_ai_get_log_detail',
6865 nonce: self.$nonce,
6866 boardid: self.$boardid,
6867 log_id: logId
6868 },
6869 success: function(response) {
6870 if (response.success) {
6871 self.$detailBody.html(response.data.html);
6872 } else {
6873 self.$detailBody.html('<div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-warning"></span><p>' + (response.data.message || 'Error loading log details') + '</p></div>');
6874 }
6875 },
6876 error: function() {
6877 self.$detailBody.html('<div class="wpforo-ai-logs-empty"><span class="dashicons dashicons-warning"></span><p>Failed to load log details</p></div>');
6878 }
6879 });
6880 },
6881
6882 showNotice: function(message, type) {
6883 var $notice = $('<div class="notice notice-' + type + ' is-dismissible"><p>' + message + '</p></div>');
6884 $('.wpforo-ai-logs-tab .wpforo-ai-box:first').before($notice);
6885
6886 // Auto dismiss after 5 seconds
6887 setTimeout(function() {
6888 $notice.fadeOut(function() {
6889 $(this).remove();
6890 });
6891 }, 5000);
6892
6893 // Make dismissible
6894 $notice.on('click', '.notice-dismiss', function() {
6895 $notice.fadeOut(function() {
6896 $(this).remove();
6897 });
6898 });
6899 },
6900
6901 formatNumber: function(num) {
6902 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
6903 }
6904 };
6905
6906 // Initialize AI Logs if on that tab
6907 $(document).ready(function() {
6908 WpForoAILogs.init();
6909
6910 // Re-init when tab is shown (in case of dynamic tab switching)
6911 $(document).on('click', '.wpforo-admin-tabs a', function() {
6912 setTimeout(function() {
6913 WpForoAILogs.init();
6914 }, 100);
6915 });
6916 });
6917
6918 window.WpForoAILogs = WpForoAILogs;
6919 });
6920