board->get_boards(); $current_boardid = isset( $_GET['boardid'] ) ? intval( $_GET['boardid'] ) : 0; // Validate boardid exists $valid_boardids = array_column( $boards, 'boardid' ); if ( ! in_array( $current_boardid, $valid_boardids ) ) { $current_boardid = 0; // Default to main board } // Switch to the selected board context if ( $current_boardid > 0 ) { WPF()->change_board( $current_boardid ); } // Render board sub-tabs if multiple boards exist if ( count( $boards ) > 1 ) : // Find current board title (from settings, not label) $current_board_title = __( 'Board', 'wpforo' ); foreach ( $boards as $board ) { if ( isset( $board['boardid'] ) && (int) $board['boardid'] === $current_boardid ) { $current_board_title = isset( $board['settings']['title'] ) ? esc_html( $board['settings']['title'] ) : ( isset( $board['title'] ) ? esc_html( $board['title'] ) : $current_board_title ); break; } } ?>
'wpforo-ai', 'tab' => 'rag_indexing', 'boardid' => $board_id, ), admin_url( 'admin.php' ) ); $active_class = ( $current_boardid === $board_id ) ? 'active' : ''; ?>
vector_storage->for_board( $current_boardid ); $storage_mode = $storage_manager->get_storage_mode(); // Get unified indexing stats (works for both local and cloud) $indexing_stats = $storage_manager->get_indexing_stats(); $total_indexed = (int) ( $indexing_stats['total_indexed'] ?? 0 ); $total_topics = (int) ( $indexing_stats['total_topics'] ?? 0 ); $indexing_progress = (int) ( $indexing_stats['indexing_progress'] ?? 0 ); $is_indexing = (bool) ( $indexing_stats['is_indexing'] ?? false ); $last_indexed_at = $indexing_stats['last_indexed_at'] ?? null; $storage_size_mb = $indexing_stats['storage_size_mb'] ?? null; // Check for pending WP Cron jobs $pending_jobs_info = $storage_manager->get_pending_cron_jobs(); $has_pending_cron_jobs = $pending_jobs_info['has_pending_jobs']; $pending_topics_count_early = $pending_jobs_info['pending_topics']; // Only show the spinner when actually indexing (backend is processing or cron batch is running). // Queued topics waiting for their scheduled time should NOT trigger the spinner. $is_processing = $is_indexing || $pending_jobs_info['is_actively_processing']; // Get storage recommendation $recommendation = $storage_manager->get_storage_recommendation(); // Local stats for display (only relevant in local mode) $local_stats = $storage_manager->is_local_mode() ? [ 'total_embeddings' => $total_indexed, 'total_topics' => $total_topics, 'storage_size_mb' => $storage_size_mb, ] : []; ?>

0 ) : ?> |
ai_client ) && WPF()->ai_client->is_feature_available( 'vector_db_cloud_storage' ); if ( $wpf_ai_cloud_storage_available ) : ?>

0 ? $local_stats['storage_size_mb'] : '0'; ?> MB


db->get_var( "SELECT COUNT(*) FROM `" . WPF()->tables->topics . "`" ); set_transient( $_ttc_cache_key, $total_topics_count, 10 * MINUTE_IN_SECONDS ); } $total_topics_count = (int) $total_topics_count; // Get available credits from status $subscription = isset( $status['subscription'] ) && is_array( $status['subscription'] ) ? $status['subscription'] : []; $credits_remaining = isset( $subscription['credits_remaining'] ) ? (int) $subscription['credits_remaining'] : 0; // Calculate remaining topics to index (not already indexed) // Note: $total_topics = unique topics with embeddings, $total_indexed = total embeddings (can be > topics due to chunking) $remaining_to_index = max( 0, $total_topics_count - $total_topics ); // Calculate how many topics can be indexed with available credits # $credits_needed = $total_topics_count; // Max credits if indexing from scratch $can_index_count = min( $remaining_to_index, $credits_remaining ); $has_enough_credits = $credits_remaining >= $remaining_to_index; $all_indexed = $remaining_to_index === 0; ?>

' . number_format( $pending_topics_count ) . '' ); ?>

0 && $credits_remaining > 0; ?>


forum->get_forums(); // Sort forums hierarchically: parents first, then children under each parent $forums_by_parent = []; foreach ( $all_forums as $forum ) { $parent = (int) ( $forum['parentid'] ?? 0 ); if ( ! isset( $forums_by_parent[ $parent ] ) ) { $forums_by_parent[ $parent ] = []; } $forums_by_parent[ $parent ][] = $forum; } // Sort children by order within each parent group foreach ( $forums_by_parent as &$children ) { usort( $children, function( $a, $b ) { return (int) ( $a['order'] ?? 0 ) - (int) ( $b['order'] ?? 0 ); } ); } unset( $children ); // Build flat list by walking the tree $sorted_forums = []; $add_forums_recursive = function( $parentid ) use ( &$add_forums_recursive, &$sorted_forums, &$forums_by_parent ) { if ( ! isset( $forums_by_parent[ $parentid ] ) ) { return; } foreach ( $forums_by_parent[ $parentid ] as $forum ) { $sorted_forums[] = $forum; $add_forums_recursive( (int) $forum['forumid'] ); } }; $add_forums_recursive( 0 ); $all_forums = $sorted_forums; // Get indexed counts from AI backend $indexed_counts = wpforo_ai_get_indexed_counts_by_forum(); // Get all forum topic counts in a single GROUP BY query // instead of calling WPF()->topic->get_count() per forum (N+1 problem) // Cache for 5 minutes to avoid heavy GROUP BY on large forums $_ftc_cache_key = 'wpforo_ai_ftc_' . $current_boardid; $forum_topic_counts = get_transient( $_ftc_cache_key ); if ( false === $forum_topic_counts ) { $forum_topic_counts = []; $_ftc_rows = WPF()->db->get_results( "SELECT `forumid`, COUNT(*) as `cnt` FROM `" . WPF()->tables->topics . "` GROUP BY `forumid`", ARRAY_A ); if ( $_ftc_rows ) { foreach ( $_ftc_rows as $_ftc_row ) { $forum_topic_counts[ (int) $_ftc_row['forumid'] ] = (int) $_ftc_row['cnt']; } } set_transient( $_ftc_cache_key, $forum_topic_counts, 5 * MINUTE_IN_SECONDS ); } if ( ! empty( $all_forums ) ) : ?>
0 ? 'wpforo-ai-forum-child' : 'wpforo-ai-forum-parent'; if ( $is_cat ) { $item_class .= ' wpforo-ai-forum-category'; } ?>


0 ) : ?>

-
-
-