')
.addClass('notice notice-' + type + ' is-dismissible')
.append($('
').text(message));
// Insert notice after page title
$('.wpforo-ai-title').after($notice);
// Auto-dismiss after 5 seconds
setTimeout(function() {
$notice.fadeOut(function() {
$(this).remove();
});
}, 5000);
// Make dismissible
$(document).trigger('wp-updates-notice-added');
},
/**
* Format numbers with thousand separators
*/
formatNumber: function(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
/**
* Validate form before submission
*/
validateForm: function($form) {
let isValid = true;
const requiredFields = $form.find('[required]');
requiredFields.each(function() {
const $field = $(this);
const value = $field.val().trim();
if (!value) {
isValid = false;
$field.addClass('error');
$field.on('input change', function() {
$(this).removeClass('error');
});
}
});
if (!isValid) {
alert('Please fill in all required fields.');
}
return isValid;
},
/**
* Initialize RAG-specific features
*/
initRAGFeatures: function() {
// Check if indexing was being stopped before page reload
if (localStorage.getItem('wpforo_indexing_stopping') === 'true') {
this.indexingStopping = true;
// Update status to show "Stopping..." if still processing
const $statusElement = $('#rag-indexing-status');
const statusText = $statusElement.text().trim();
// Only show "Stopping..." if status indicates processing (not idle)
if ($statusElement.length && statusText !== 'Idle') {
$statusElement.text('Stopping...');
} else if (statusText === 'Idle') {
// Process already stopped, clear the flag
this.indexingStopping = false;
localStorage.removeItem('wpforo_indexing_stopping');
}
}
// Unbind first to prevent duplicate handlers
$(document).off('click', '.wpforo-ai-reindex-all');
$(document).off('click', '.wpforo-ai-reindex-images');
$(document).off('click', '.wpforo-ai-clear-database');
$(document).off('click', '.wpforo-ai-clear-and-reindex');
$(document).off('click', '.wpforo-ai-stop-indexing');
$(document).off('click', '.wpforo-ai-cleanup-session');
$(document).off('submit', '#wpforo-ai-search-test-form');
// Bind bulk action buttons
$(document).on('click', '.wpforo-ai-reindex-all', this.handleReindexAll.bind(this));
$(document).on('click', '.wpforo-ai-reindex-images', this.handleReindexImages.bind(this));
$(document).on('click', '.wpforo-ai-clear-database', this.handleClearDatabase.bind(this));
$(document).on('click', '.wpforo-ai-clear-and-reindex', this.handleClearAndReindex.bind(this));
$(document).on('click', '.wpforo-ai-stop-indexing', this.handleStopIndexing.bind(this));
$(document).on('click', '.wpforo-ai-cleanup-session', this.handleCleanupSession.bind(this));
// Bind search test form
$(document).on('submit', '#wpforo-ai-search-test-form', this.handleSearchTest.bind(this));
// Bind storage mode toggle
$(document).off('change', 'input[name="wpforo_ai_storage_mode"]');
$(document).on('change', 'input[name="wpforo_ai_storage_mode"]', this.handleStorageModeChange.bind(this));
// Bind auto-indexing toggle
$(document).off('change', '#wpforo-ai-auto-indexing');
$(document).on('change', '#wpforo-ai-auto-indexing', this.handleAutoIndexingToggle.bind(this));
// Bind image indexing toggle
$(document).off('change', '#wpforo-ai-image-indexing');
$(document).on('change', '#wpforo-ai-image-indexing', this.handleImageIndexingToggle.bind(this));
// Bind document indexing toggle
$(document).off('change', '#wpforo-ai-document-indexing');
$(document).on('change', '#wpforo-ai-document-indexing', this.handleDocumentIndexingToggle.bind(this));
// Bind refresh status button
$(document).off('click', '.wpforo-ai-refresh-rag-status');
$(document).on('click', '.wpforo-ai-refresh-rag-status', this.handleRefreshStatus.bind(this));
// WordPress Content Indexing handlers
this.initWordPressIndexingFeatures();
// Check for in-progress local indexing and auto-resume
this.checkLocalIndexingProgress();
// Note: Polling is started from PHP inline script based on server-side $is_indexing status
// No need to start it here to avoid duplicate polling
},
/**
* Handle storage mode toggle change
*/
handleStorageModeChange: function(e) {
const $input = $(e.currentTarget);
const newMode = $input.val();
const $container = $input.closest('.wpforo-ai-storage-toggle');
// Update active state on labels
$container.find('.wpforo-ai-storage-option').removeClass('active');
$input.next('label').addClass('active');
// Get the current board ID from URL
const urlParams = new URLSearchParams(window.location.search);
const boardId = urlParams.get('boardid') || 0;
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_storage_mode',
nonce: wpforoAIAdmin.nonce,
storage_mode: newMode,
board_id: boardId
},
beforeSend: function() {
$container.css('opacity', '0.6');
},
success: function(response) {
$container.css('opacity', '1');
if (response.success) {
// Reload page to update storage info section
window.location.reload();
} else {
alert(response.data?.message || 'Failed to save storage mode.');
// Revert the change
window.location.reload();
}
},
error: function() {
$container.css('opacity', '1');
alert('Error saving storage mode. Please try again.');
window.location.reload();
}
});
},
/**
* Handle auto-indexing toggle change
*/
handleAutoIndexingToggle: function(e) {
const $input = $(e.currentTarget);
const isEnabled = $input.is(':checked') ? 1 : 0;
const boardId = $input.data('board-id') || 0;
const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
// Disable the toggle during AJAX request
$input.prop('disabled', true);
$toggle.css('opacity', '0.6');
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_auto_indexing',
nonce: wpforoAIAdmin.nonce,
enabled: isEnabled,
board_id: boardId
},
success: function(response) {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
if (!response.success) {
// Revert the change on failure
$input.prop('checked', !isEnabled);
alert(response.data?.message || 'Failed to save auto-indexing setting.');
}
},
error: function() {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
// Revert the change on error
$input.prop('checked', !isEnabled);
alert('Error saving auto-indexing setting. Please try again.');
}
});
},
/**
* Handle image indexing toggle change
*
* When enabled, posts with images will consume +1 additional credit
* for multimodal processing (image → text → embedding).
* Requires Business or Enterprise plan.
*/
handleImageIndexingToggle: function(e) {
const $input = $(e.currentTarget);
const isEnabled = $input.is(':checked') ? 1 : 0;
const boardId = $input.data('board-id') || 0;
const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
// Show confirmation when enabling (due to credit impact)
if (isEnabled) {
const confirmed = confirm(
'Enable Image Indexing?\n\n' +
'When enabled, posts with images will consume +1 additional credit during indexing.\n\n' +
'• Maximum 10 images per post are processed\n' +
'• Images are converted to text descriptions for search\n' +
'• Small images (< 50x50px) like smileys are skipped\n\n' +
'Continue?'
);
if (!confirmed) {
$input.prop('checked', false);
return;
}
}
// Disable the toggle during AJAX request
$input.prop('disabled', true);
$toggle.css('opacity', '0.6');
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_image_indexing',
nonce: wpforoAIAdmin.nonce,
enabled: isEnabled,
board_id: boardId
},
success: function(response) {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
if (response.success) {
// Show success message
if (response.data?.message) {
// Brief notification instead of alert
console.log('Image indexing: ' + response.data.message);
}
} else {
// Revert the change on failure
$input.prop('checked', !isEnabled);
alert(response.data?.message || 'Failed to save image indexing setting.');
}
},
error: function() {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
// Revert the change on error
$input.prop('checked', !isEnabled);
alert('Error saving image indexing setting. Please try again.');
}
});
},
/**
* Handle document indexing toggle change
*/
handleDocumentIndexingToggle: function(e) {
const $input = $(e.currentTarget);
const isEnabled = $input.is(':checked') ? 1 : 0;
const boardId = $input.data('board-id') || 0;
const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
// Show confirmation when enabling (due to credit impact)
if (isEnabled) {
const confirmed = confirm(
'Enable Document Indexing?\n\n' +
'When enabled, document attachments (PDF, DOCX, PPTX, etc.) will be processed during indexing.\n\n' +
'• Maximum 5 documents per post\n' +
'• Text is extracted from documents for search\n' +
'• Credit cost: 1 per page\n\n' +
'Continue?'
);
if (!confirmed) {
$input.prop('checked', false);
return;
}
}
// Disable the toggle during AJAX request
$input.prop('disabled', true);
$toggle.css('opacity', '0.6');
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_document_indexing',
nonce: wpforoAIAdmin.nonce,
enabled: isEnabled,
board_id: boardId
},
success: function(response) {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
if (response.success) {
if (response.data?.message) {
console.log('Document indexing: ' + response.data.message);
}
} else {
$input.prop('checked', !isEnabled);
alert(response.data?.message || 'Failed to save document indexing setting.');
}
},
error: function() {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
$input.prop('checked', !isEnabled);
alert('Error saving document indexing setting. Please try again.');
}
});
},
/**
* Handle refresh status button click
*/
handleRefreshStatus: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const $icon = $button.find('.dashicons-update');
// Add spinning animation
$icon.addClass('wpforo-spin');
$button.prop('disabled', true);
// Store reference for callback
const self = this;
// Refresh status via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_get_rag_status',
nonce: wpforoAIAdmin.nonce
},
success: function(response) {
if (response.success && response.data) {
self.updateRAGStatusDisplay(response.data);
}
},
error: function(xhr, status, error) {
console.error('Failed to refresh RAG status:', error);
},
complete: function() {
// Stop spinning animation
$icon.removeClass('wpforo-spin');
$button.prop('disabled', false);
}
});
},
// =====================================================
// WordPress Content Indexing Methods
// =====================================================
/**
* Initialize WordPress content indexing features
*/
initWordPressIndexingFeatures: function() {
const self = this;
// Unbind first to prevent duplicate handlers
$(document).off('click', '.wpforo-ai-refresh-wp-status');
$(document).off('change', '#wp-taxonomy-select');
$(document).off('submit', '.wpforo-ai-wp-taxonomy-form');
$(document).off('submit', '.wpforo-ai-wp-custom-form');
$(document).off('submit', '.wpforo-ai-wp-ids-form');
$(document).off('click', '.wpforo-ai-wp-clear-index');
$(document).off('click', '.wpforo-ai-select-all-terms');
$(document).off('click', '.wpforo-ai-deselect-all-terms');
$(document).off('change', '#wpforo-ai-wp-auto-indexing');
$(document).off('change', '#wpforo-ai-wp-image-indexing');
// Bind event handlers
$(document).on('click', '.wpforo-ai-refresh-wp-status', this.handleRefreshWPStatus.bind(this));
$(document).on('change', '#wp-taxonomy-select', this.handleTaxonomyChange.bind(this));
$(document).on('submit', '.wpforo-ai-wp-taxonomy-form', this.handleWPTaxonomyIndex.bind(this));
$(document).on('submit', '.wpforo-ai-wp-custom-form', this.handleWPCustomIndex.bind(this));
$(document).on('submit', '.wpforo-ai-wp-ids-form', this.handleWPIndexByIds.bind(this));
$(document).on('click', '.wpforo-ai-wp-clear-index', this.handleWPClearIndex.bind(this));
// WordPress-specific auto-indexing and image indexing toggles
$(document).on('change', '#wpforo-ai-wp-auto-indexing', this.handleWPAutoIndexingToggle.bind(this));
$(document).on('change', '#wpforo-ai-wp-image-indexing', this.handleWPImageIndexingToggle.bind(this));
// Select All / Deselect All for terms
$(document).on('click', '.wpforo-ai-select-all-terms', function() {
$('#wp-terms-container input[type="checkbox"]').prop('checked', true);
self.updateTermIndexButton();
});
$(document).on('click', '.wpforo-ai-deselect-all-terms', function() {
$('#wp-terms-container input[type="checkbox"]').prop('checked', false);
self.updateTermIndexButton();
});
// Load initial WordPress indexing status
if ($('.wpforo-ai-wordpress-indexing-box').length) {
this.loadWPIndexingStatus();
}
},
/**
* Refresh WordPress indexing status
*/
handleRefreshWPStatus: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const $icon = $button.find('.dashicons-update');
$icon.addClass('wpforo-spin');
$button.prop('disabled', true);
this.loadWPIndexingStatus(function() {
$icon.removeClass('wpforo-spin');
$button.prop('disabled', false);
});
},
// Polling interval for WordPress content indexing
wpIndexingPollInterval: null,
/**
* Load WordPress indexing status from API
*/
loadWPIndexingStatus: function(callback) {
const self = this;
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_wp_get_indexing_status',
security: wpforoAIAdmin.adminNonce
},
success: function(response) {
if (response.success && response.data) {
self.updateWPIndexingDisplay(response.data);
// Start polling if indexing is in progress
if (response.data.queue && response.data.queue.status === 'processing') {
self.startWPIndexingPolling();
} else {
self.stopWPIndexingPolling();
}
}
},
error: function(xhr, status, error) {
console.error('Failed to load WordPress indexing status:', error);
},
complete: function() {
if (typeof callback === 'function') {
callback();
}
}
});
},
/**
* Start polling for WordPress indexing status
*/
startWPIndexingPolling: function() {
const self = this;
// Don't start if already polling
if (this.wpIndexingPollInterval) {
return;
}
// Poll every 5 seconds
this.wpIndexingPollInterval = setInterval(function() {
self.loadWPIndexingStatus();
}, 5000);
},
/**
* Stop polling for WordPress indexing status
*/
stopWPIndexingPolling: function() {
if (this.wpIndexingPollInterval) {
clearInterval(this.wpIndexingPollInterval);
this.wpIndexingPollInterval = null;
}
},
/**
* Update WordPress indexing display with status data
*/
updateWPIndexingDisplay: function(data) {
// Update total indexed
if (data.total_indexed !== undefined) {
$('#wp-total-indexed').text(data.total_indexed.toLocaleString());
}
// Update by_type counts
if (data.by_type) {
for (const [type, info] of Object.entries(data.by_type)) {
const postType = type.replace('wp_', '');
const $indexed = $('#wp-indexed-' + postType + ' .indexed-count');
if ($indexed.length) {
$indexed.text(info.indexed || 0);
}
}
}
// Get status elements
const $statusElement = $('#wp-indexing-status');
const $statusIcon = $statusElement.closest('.rag-stat-item').find('.stat-icon .dashicons');
// Update status with spinner animation
if (data.queue && data.queue.status === 'processing') {
$statusElement.text(wpforoAIAdmin.strings?.indexing || 'Indexing...');
// Add spinning animation to icon
$statusIcon
.removeClass('dashicons-saved')
.addClass('dashicons-update wpforo-wp-indexing-spin');
this.showWPProgress(data.queue);
} else {
$statusElement.text(wpforoAIAdmin.strings?.idle || 'Idle');
// Stop spinning, show checkmark
$statusIcon
.removeClass('dashicons-update wpforo-wp-indexing-spin')
.addClass('dashicons-saved');
$('.wpforo-ai-wp-progress').hide();
}
},
/**
* Show WordPress indexing progress bar
*/
showWPProgress: function(queue) {
const $progress = $('.wpforo-ai-wp-progress');
const percent = queue.total > 0 ? Math.round((queue.current / queue.total) * 100) : 0;
$progress.show();
$progress.find('.progress-fill').css('width', percent + '%');
$progress.find('.progress-percent').text(percent + '%');
$progress.find('.progress-status').text(
(queue.indexed || 0) + ' indexed, ' + (queue.failed || 0) + ' failed'
);
},
/**
* Handle taxonomy dropdown change - load terms as checkboxes
*/
handleTaxonomyChange: function(e) {
const self = this;
const taxonomy = $(e.currentTarget).val();
const $termsContainer = $('#wp-terms-container');
const $termsActions = $('#wp-terms-actions');
const $indexBtn = $('.wpforo-ai-wp-index-taxonomy');
if (!taxonomy) {
$termsContainer.html('
Select a taxonomy first to load terms...
');
$termsActions.hide();
$indexBtn.prop('disabled', true);
return;
}
$termsContainer.html('
Loading terms...
');
$termsActions.hide();
// Note: post_types are not passed here - the backend will auto-detect
// the post types that use this taxonomy and count only published posts
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_wp_get_taxonomy_terms',
security: wpforoAIAdmin.adminNonce,
taxonomy: taxonomy
},
success: function(response) {
if (response.success && response.data && response.data.terms) {
const terms = response.data.terms;
if (terms.length === 0) {
$termsContainer.html('
No terms found in this taxonomy.
');
$termsActions.hide();
$indexBtn.prop('disabled', true);
return;
}
let html = '
';
terms.forEach(function(term) {
const indexed = term.indexed || 0;
const total = term.count || 0;
html += self.renderTermCheckbox(term, indexed, total, false);
// Add children if any
if (term.children && term.children.length) {
term.children.forEach(function(child) {
const childIndexed = child.indexed || 0;
const childTotal = child.count || 0;
html += self.renderTermCheckbox(child, childIndexed, childTotal, true);
});
}
});
html += '
';
$termsContainer.html(html);
$termsActions.show();
// Bind checkbox change events
$termsContainer.find('input[type="checkbox"]').on('change', function() {
self.updateTermIndexButton();
});
self.updateTermIndexButton();
} else {
$termsContainer.html('
Error loading terms.
');
$termsActions.hide();
}
},
error: function() {
$termsContainer.html('
Error loading terms.
');
$termsActions.hide();
}
});
},
/**
* Render a single term checkbox item
*/
renderTermCheckbox: function(term, indexed, total, isChild) {
const itemClass = isChild ? 'wpforo-ai-term-checkbox-item wpforo-ai-term-child' : 'wpforo-ai-term-checkbox-item';
return '
' +
' ' +
'' + this.escapeHtml(term.name) + ' ' +
'(' + indexed + '/' + total + ') ' +
' ';
},
/**
* Update the index button state based on selected terms
*/
updateTermIndexButton: function() {
const $indexBtn = $('.wpforo-ai-wp-index-taxonomy');
const checkedCount = $('#wp-terms-container input[type="checkbox"]:checked').length;
$indexBtn.prop('disabled', checkedCount === 0);
},
/**
* Escape HTML special characters
*/
escapeHtml: function(text) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
},
/**
* Handle taxonomy-based indexing form submission
*/
handleWPTaxonomyIndex: function(e) {
e.preventDefault();
const $form = $(e.currentTarget);
const $button = $form.find('.wpforo-ai-wp-index-taxonomy');
const taxonomy = $form.find('#wp-taxonomy-select').val();
// Collect all selected term IDs from checkboxes
const termIds = [];
$('#wp-terms-container input[type="checkbox"]:checked').each(function() {
termIds.push($(this).val());
});
if (!taxonomy || termIds.length === 0) {
alert('Please select a taxonomy and at least one term.');
return;
}
// Get selected post types
const postTypes = [];
$('.wpforo-ai-wp-type-checkbox:checked').each(function() {
postTypes.push($(this).val());
});
if (postTypes.length === 0) {
alert('Please select at least one content type.');
return;
}
$button.prop('disabled', true).text('Indexing...');
// Build request data including optional date range
const requestData = {
action: 'wpforo_ai_wp_index_by_taxonomy',
security: wpforoAIAdmin.adminNonce,
taxonomy: taxonomy,
term_ids: termIds,
post_types: postTypes
};
// Add date range if specified
const dateFrom = $form.find('#wp-tax-date-from').val();
const dateTo = $form.find('#wp-tax-date-to').val();
if (dateFrom) requestData.date_from = dateFrom;
if (dateTo) requestData.date_to = dateTo;
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: requestData,
success: function(response) {
if (response.success) {
alert('Indexing queued: ' + response.data.total_posts + ' posts in ' + response.data.batches + ' batches.');
// Start polling for progress
WpForoAI.loadWPIndexingStatus();
} else {
alert('Error: ' + (response.data?.message || 'Unknown error'));
}
},
error: function() {
alert('Error starting indexing. Please try again.');
},
complete: function() {
$button.prop('disabled', false).html('
Index Selected Terms');
}
});
},
/**
* Handle custom indexing form submission
*/
handleWPCustomIndex: function(e) {
e.preventDefault();
const $form = $(e.currentTarget);
const $button = $form.find('.wpforo-ai-wp-index-custom');
// Get selected post types from within this form
const postTypes = [];
$form.find('.wpforo-ai-wp-type-checkbox:checked').each(function() {
postTypes.push($(this).val());
});
if (postTypes.length === 0) {
alert('Please select at least one content type.');
return;
}
const data = {
action: 'wpforo_ai_wp_index_custom',
security: wpforoAIAdmin.adminNonce,
post_types: postTypes,
date_from: $form.find('#wp-date-from').val(),
date_to: $form.find('#wp-date-to').val()
};
$button.prop('disabled', true).text('Indexing...');
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: data,
success: function(response) {
if (response.success) {
alert('Indexing queued: ' + response.data.total_posts + ' posts in ' + response.data.batches + ' batches.');
WpForoAI.loadWPIndexingStatus();
} else {
alert('Error: ' + (response.data?.message || 'Unknown error'));
}
},
error: function() {
alert('Error starting indexing. Please try again.');
},
complete: function() {
$button.prop('disabled', false).html('
Index Selected Content');
}
});
},
/**
* Handle index by specific IDs form submission
*/
handleWPIndexByIds: function(e) {
e.preventDefault();
const $form = $(e.currentTarget);
const $button = $form.find('.wpforo-ai-wp-index-ids');
const postIds = $form.find('#wp-post-ids').val().trim();
if (!postIds) {
alert('Please enter at least one post ID.');
return;
}
const data = {
action: 'wpforo_ai_wp_index_custom',
security: wpforoAIAdmin.adminNonce,
post_ids: postIds
};
$button.prop('disabled', true).text('Indexing...');
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: data,
success: function(response) {
if (response.success) {
alert('Indexing queued: ' + response.data.total_posts + ' posts in ' + response.data.batches + ' batches.');
WpForoAI.loadWPIndexingStatus();
$form.find('#wp-post-ids').val(''); // Clear the field
} else {
alert('Error: ' + (response.data?.message || 'Unknown error'));
}
},
error: function() {
alert('Error starting indexing. Please try again.');
},
complete: function() {
$button.prop('disabled', false).html('
Index by IDs');
}
});
},
/**
* Handle Clear WordPress index button
*/
handleWPClearIndex: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = $button.data('confirm');
if (!confirm(confirmMessage)) {
return;
}
$button.prop('disabled', true).text('Clearing...');
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_wp_delete_content',
security: wpforoAIAdmin.adminNonce,
delete_all: 'true'
},
success: function(response) {
if (response.success) {
alert('WordPress index cleared successfully.');
WpForoAI.loadWPIndexingStatus();
} else {
alert('Error: ' + (response.data?.message || 'Unknown error'));
}
},
error: function() {
alert('Error clearing index. Please try again.');
},
complete: function() {
$button.prop('disabled', false).html('
Clear WordPress Index');
}
});
},
/**
* Handle WordPress auto-indexing toggle change
*/
handleWPAutoIndexingToggle: function(e) {
const $input = $(e.currentTarget);
const isEnabled = $input.is(':checked') ? 1 : 0;
const optionName = $input.data('option-name') || 'ai_wp_auto_indexing_enabled';
const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
// Disable the toggle during AJAX request
$input.prop('disabled', true);
$toggle.css('opacity', '0.6');
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_wp_indexing_option',
nonce: wpforoAIAdmin.nonce,
option_name: optionName,
enabled: isEnabled
},
success: function(response) {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
if (!response.success) {
// Revert the change on failure
$input.prop('checked', !isEnabled);
alert('Error: ' + (response.data?.message || 'Failed to save setting'));
}
},
error: function() {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
$input.prop('checked', !isEnabled);
alert('Error saving setting. Please try again.');
}
});
},
/**
* Handle WordPress image indexing toggle change
*/
handleWPImageIndexingToggle: function(e) {
const $input = $(e.currentTarget);
const isEnabled = $input.is(':checked') ? 1 : 0;
const optionName = $input.data('option-name') || 'ai_wp_image_indexing_enabled';
const $toggle = $input.closest('.wpforo-ai-auto-index-toggle');
// Show confirmation when enabling (due to credit impact)
if (isEnabled) {
const confirmed = confirm(
'Enable Image Indexing for WordPress Content?\n\n' +
'When enabled, posts with images will consume +1 additional credit during indexing.\n\n' +
'• Maximum 10 images per post are processed\n' +
'• Images are converted to text descriptions for search\n' +
'• Small images (< 50x50px) like smileys are skipped\n\n' +
'Continue?'
);
if (!confirmed) {
$input.prop('checked', false);
return;
}
}
// Disable the toggle during AJAX request
$input.prop('disabled', true);
$toggle.css('opacity', '0.6');
// Save via AJAX
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_save_wp_indexing_option',
nonce: wpforoAIAdmin.nonce,
option_name: optionName,
enabled: isEnabled
},
success: function(response) {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
if (!response.success) {
// Revert the change on failure
$input.prop('checked', !isEnabled);
alert('Error: ' + (response.data?.message || 'Failed to save setting'));
}
},
error: function() {
$input.prop('disabled', false);
$toggle.css('opacity', '1');
$input.prop('checked', !isEnabled);
alert('Error saving setting. Please try again.');
}
});
},
/**
* Initialize tag autocomplete using WordPress suggest script
*/
initTagSuggest: function() {
var $tagInput = $('.wpforo-ai-tags-input');
if ($tagInput.length && typeof $.fn.suggest === 'function' && typeof wpforoAIAdmin !== 'undefined') {
var ajaxUrl = wpforoAIAdmin.ajaxUrl;
$tagInput.suggest(
ajaxUrl + (ajaxUrl.indexOf('?') !== -1 ? '&' : '?') + 'action=wpforo_tag_search',
{
multiple: true,
multipleSep: ',',
delay: 500,
minchars: 2,
resultsClass: 'wpforo-ai-tag-results',
selectClass: 'wpforo-ai-tag-over',
matchClass: 'wpforo-ai-tag-match'
}
);
}
},
/**
* Initialize Bot User Search autocomplete for AI Bot Reply settings
*/
initBotUserSearch: function() {
const self = this;
const $searchInput = $('#wpforo-ai-bot-user-search');
// Only init if the search input exists (settings page with Bot Reply section)
if (!$searchInput.length) {
return;
}
const $wrapper = $searchInput.closest('.wpforo-ai-user-search-wrapper');
const $hiddenInput = $wrapper.find('.wpforo-ai-user-id-input');
const $resultsContainer = $wrapper.find('.wpforo-ai-user-search-results');
const nonce = $('#wpforo_ai_bot_user_nonce').val() || '';
let searchTimeout = null;
// Handle input for search
$searchInput.on('input', function() {
const searchTerm = $(this).val().trim();
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Clear results if search term is too short
if (searchTerm.length < 2) {
$resultsContainer.empty().hide();
return;
}
// Debounce the search
searchTimeout = setTimeout(function() {
self.searchBotUsers(searchTerm, $resultsContainer, $hiddenInput, $searchInput, nonce);
}, 300);
});
// Handle click outside to close results
$(document).on('click', function(e) {
if (!$(e.target).closest('.wpforo-ai-user-search-wrapper').length) {
$resultsContainer.empty().hide();
}
});
// Handle focus to show results if there's a search term
$searchInput.on('focus', function() {
if ($(this).val().trim().length >= 2 && $resultsContainer.children().length > 0) {
$resultsContainer.show();
}
});
},
/**
* Perform AJAX search for bot users
*/
searchBotUsers: function(searchTerm, $resultsContainer, $hiddenInput, $searchInput, nonce) {
$resultsContainer.html('
Searching...
').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpforo_ai_search_bot_users',
search: searchTerm,
_wpnonce: nonce
},
success: function(response) {
$resultsContainer.empty();
if (response.success && response.data.users && response.data.users.length > 0) {
const $list = $('
');
response.data.users.forEach(function(user) {
const $item = $('
');
$item.text(user.label);
$item.on('click', function() {
$hiddenInput.val(user.id);
$searchInput.val(user.label);
$resultsContainer.empty().hide();
// Clear usergroup when specific user is selected
$hiddenInput.closest('.wpforo-ai-form-section').find('.wpforo-ai-author-groupid-select').val('');
});
$list.append($item);
});
$resultsContainer.append($list).show();
} else {
$resultsContainer.html('
No users found
').show();
}
},
error: function() {
$resultsContainer.html('
Search error
').show();
}
});
},
/**
* Initialize character counters for textareas with limits
* Uses proper character counting that works with multibyte characters
*/
initCharCounters: function() {
const self = this;
// Find all textareas with data-char-limit attribute
$(document).on('input', 'textarea[data-char-limit]', function() {
self.updateCharCounter($(this));
});
// Also handle when form fields are populated (e.g., when editing a task)
$(document).on('wpforo-ai-task-loaded', function() {
$('textarea[data-char-limit]').each(function() {
self.updateCharCounter($(this));
});
});
// Initialize counters on page load
$('textarea[data-char-limit]').each(function() {
self.updateCharCounter($(this));
});
},
/**
* Update character counter for a textarea
* Uses string spread operator for proper Unicode character counting
*/
updateCharCounter: function($textarea) {
const limit = parseInt($textarea.data('char-limit'), 10) || 120;
const $counter = $textarea.siblings('.wpforo-ai-char-counter').find('.current');
const $counterWrapper = $textarea.siblings('.wpforo-ai-char-counter');
if (!$counter.length) {
return;
}
// Use spread operator to properly count Unicode characters (multibyte safe)
const text = $textarea.val() || '';
const charCount = [...text].length;
$counter.text(charCount);
// Update counter styling based on proximity to limit
$counterWrapper.removeClass('warning limit');
if (charCount >= limit) {
$counterWrapper.addClass('limit');
} else if (charCount >= limit * 0.8) {
$counterWrapper.addClass('warning');
}
// Enforce limit (multibyte safe truncation)
if (charCount > limit) {
const truncated = [...text].slice(0, limit).join('');
$textarea.val(truncated);
$counter.text(limit);
$counterWrapper.addClass('limit');
}
},
/**
* Scroll to the Indexing Status section
*/
scrollToIndexingStatus: function() {
const $statusBox = $('.wpforo-ai-rag-status-box');
if ($statusBox.length) {
$('html, body').animate({
scrollTop: $statusBox.offset().top - 50
}, 500);
}
},
/**
* Handle Re-Index All button click
*/
handleReindexAll: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = $button.data('confirm');
if (!confirm(confirmMessage)) {
return;
}
// Scroll to status section
this.scrollToIndexingStatus();
// Check if we're in local storage mode
if (this.isLocalStorageMode()) {
// Use AJAX-driven batch processing for local mode
this.startLocalIndexing($button);
} else {
// Use form submission for cloud mode
this.submitRAGAction('reindex_all', $button);
}
},
/**
* Handle Re-Index Topic Images button click
* Only re-indexes topics that contain images
*/
handleReindexImages: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = $button.data('confirm');
if (!confirm(confirmMessage)) {
return;
}
// Scroll to status section
this.scrollToIndexingStatus();
// Check if we're in local storage mode
if (this.isLocalStorageMode()) {
// Use AJAX-driven batch processing for local mode with images_only flag
this.startLocalIndexing($button, { images_only: true });
} else {
// Use form submission for cloud mode with images_only flag
this.submitRAGAction('reindex_images', $button);
}
},
/**
* Handle Clear Database button click
*/
handleClearDatabase: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = 'WARNING: This will permanently delete all indexed data.\n\nType "DELETE" to confirm:';
const userInput = prompt(confirmMessage);
if (userInput !== 'DELETE') {
if (userInput !== null) {
alert('Confirmation failed. Database was not cleared.');
}
return;
}
// Create and submit form with confirmation value
this.submitRAGAction('clear_database', $button, { confirm: userInput });
},
/**
* Handle Clear & Re-Index button click
*/
handleClearAndReindex: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = 'This will:\n1. Clear all indexed data\n2. Re-index all topics\n\nType "CONFIRM" to proceed:';
const userInput = prompt(confirmMessage);
if (userInput !== 'CONFIRM') {
if (userInput !== null) {
alert('Confirmation failed. Operation cancelled.');
}
return;
}
// Check if we're in local storage mode
if (this.isLocalStorageMode()) {
// Use AJAX-driven process for local mode
this.clearAndReindexLocal($button);
} else {
// Use form submission for cloud mode
this.submitRAGAction('clear_and_reindex', $button);
}
},
/**
* Clear and re-index for local storage mode via AJAX
*/
clearAndReindexLocal: function($button) {
const self = this;
// Show loading state
$button.addClass('loading').prop('disabled', true);
$button.html('
Clearing...');
// First clear local embeddings
$.ajax({
url: wpforoAIAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wpforo_ai_action',
wpforo_ai_action: 'clear_local_embeddings',
_wpnonce: wpforoAIAdmin.nonce
},
success: function(response) {
if (response.success) {
console.log('Local embeddings cleared:', response.data);
// Now start the indexing
self.startLocalIndexing($button);
} else {
const errorMsg = response.data && response.data.message
? response.data.message
: 'Failed to clear embeddings';
alert('Error: ' + errorMsg);
$button.removeClass('loading').prop('disabled', false);
$button.html('
Clear & Re-Index');
}
},
error: function(xhr, status, error) {
console.error('Clear local embeddings error:', error);
alert('Error clearing embeddings: ' + error);
$button.removeClass('loading').prop('disabled', false);
$button.html('
Clear & Re-Index');
}
});
},
/**
* Handle Stop Indexing button click
*/
handleStopIndexing: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const confirmMessage = $button.data('confirm');
if (!confirm(confirmMessage)) {
return;
}
// Set stopping flag so status shows "Stopping..." while process winds down
// Use localStorage to persist across page reloads
this.indexingStopping = true;
localStorage.setItem('wpforo_indexing_stopping', 'true');
// Immediately update status to show "Stopping..."
const $statusElement = $('#rag-indexing-status');
if ($statusElement.length) {
$statusElement.text('Stopping...');
}
// Check if we're in local storage mode with AJAX indexing
if (this.isLocalStorageMode() && this.localIndexingState) {
// Stop the AJAX-driven indexing loop (this updates UI)
this.stopLocalIndexing();
// Clear the queue on the server via AJAX (no page reload)
this.clearLocalIndexingQueue();
} else {
// Cloud mode: tell the backend to stop the image_worker
// draining queued media jobs. Polling will pick up the
// drained state via the regular /rag/status poll.
this.cancelCloudIndexing();
}
},
/**
* Tell the backend to stop in-flight cloud indexing (image worker).
* No page reload — polling will pick up the drained state.
*/
cancelCloudIndexing: function() {
const self = this;
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpforo_ai_cancel_cloud_indexing',
_wpnonce: wpforoAIAdmin.nonce
},
success: function(response) {
console.log('Cloud indexing cancel requested:', response);
},
error: function(xhr, status, error) {
console.error('Failed to cancel cloud indexing:', error);
// Clear the stopping flag so the user can retry
self.indexingStopping = false;
localStorage.removeItem('wpforo_indexing_stopping');
}
});
},
/**
* Handle "Cleanup Indexing Session" button clicks.
*
* Resets stuck indexing state (queues, WP-Cron jobs, transient locks,
* status caches) without touching any already-indexed data. Works for
* both local and cloud storage modes — the backend cleans up both
* queue keys in one call and also tells the cloud image_worker to
* drop any in-flight messages.
*
* Also clears the browser-side localStorage stopping flag so the UI
* doesn't get stuck on "Stopping..." after the cleanup.
*
* data-scope on the button is 'forum' or 'wp'.
*/
handleCleanupSession: function(e) {
e.preventDefault();
const $button = $(e.currentTarget);
const scope = $button.data('scope') || 'forum';
const confirmMsg = $button.data('confirm') || 'Reset stuck indexing session?';
if (!window.confirm(confirmMsg)) {
return;
}
const originalHtml = $button.html();
$button.prop('disabled', true).html('
Cleaning up...');
// Clear any browser-side stuck state first — regardless of AJAX
// outcome. This is the only client-side flag the plugin sets for
// indexing (see handleStopIndexing / checkLocalIndexingProgress).
try {
localStorage.removeItem('wpforo_indexing_stopping');
} catch (err) { /* localStorage may be blocked in some contexts */ }
this.indexingStopping = false;
const self = this;
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpforo_ai_cleanup_indexing_session',
scope: scope,
_wpnonce: wpforoAIAdmin.nonce
},
success: function(response) {
$button.prop('disabled', false).html(originalHtml);
if (response && response.success) {
// Reload to refresh all server-rendered counts and
// flip the UI out of "Indexing..." state cleanly.
window.location.reload();
} else {
const msg = (response && response.data && response.data.message) || 'Cleanup failed.';
window.alert(msg);
}
},
error: function(xhr, status, error) {
$button.prop('disabled', false).html(originalHtml);
console.error('Cleanup indexing session failed:', error);
window.alert('Cleanup failed. Check the browser console for details.');
}
});
},
/**
* Clear local indexing queue via AJAX (no page reload)
*/
clearLocalIndexingQueue: function() {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpforo_ai_action',
wpforo_ai_action: 'stop_local_indexing',
_wpnonce: wpforoAIAdmin.nonce
},
success: function(response) {
console.log('Local indexing queue cleared:', response);
},
error: function(xhr, status, error) {
console.error('Failed to clear queue:', error);
}
});
},
/**
* Submit RAG action form
*/
submitRAGAction: function(action, $button, additionalData) {
// Create hidden form
const $form = $('