PluginProbe
wpForo Forum / 3.0.2
wpForo Forum v3.0.2
3.1.7 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 All 139 releases
wpforo / admin / assets / js / ai-features.js

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

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