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' => 'analytics', 'boardid' => $board_id, ), admin_url( 'admin.php' ) ); $active_class = ( $current_boardid === $board_id ) ? 'active' : ''; ?>
__( 'AI Usage Analytics', 'wpforo' ), 'forum_activity' => __( 'Forum Activity', 'wpforo' ), 'user_engagement' => __( 'User Engagement', 'wpforo' ), 'content' => __( 'Content Performance', 'wpforo' ), 'ai_insights' => __( 'AI Insights', 'wpforo' ), ); ?>
__( 'Today', 'wpforo' ), '7days' => __( 'Last 7 Days', 'wpforo' ), '30days' => __( 'Last 30 Days', 'wpforo' ), '90days' => __( 'Last 90 Days', 'wpforo' ), 'this_year' => __( 'This Year', 'wpforo' ), 'all_time' => __( 'All Time', 'wpforo' ), 'custom' => __( 'Custom Range', 'wpforo' ), ); $current_range = isset( $_GET['range'] ) ? sanitize_key( $_GET['range'] ) : '7days'; $custom_start = isset( $_GET['start_date'] ) ? sanitize_text_field( $_GET['start_date'] ) : ''; $custom_end = isset( $_GET['end_date'] ) ? sanitize_text_field( $_GET['end_date'] ) : ''; // Calculate time range $time_info = wpforo_ai_calculate_time_range( $current_range, $custom_start, $custom_end ); ?>
$range_label ) : if ( $range_key === 'custom' ) continue; // Handle custom separately $range_url = add_query_arg( array( 'page' => 'wpforo-ai', 'tab' => 'analytics', 'subtab' => 'ai_usage', 'boardid' => $board_id, 'range' => $range_key, ), admin_url( 'admin.php' ) ); $active_class = ( $current_range === $range_key ) ? 'active' : ''; ?>

$board_id, 'startTime' => $time_info['start'], 'endTime' => $time_info['end'], 'range' => $current_range, 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforo_ai_analytics_nonce' ), 'i18n' => array( 'loading' => __( 'Loading...', 'wpforo' ), 'noData' => __( 'No data available for this period', 'wpforo' ), 'error' => __( 'Error loading data', 'wpforo' ), 'credits' => __( 'Credits', 'wpforo' ), 'requests' => __( 'Requests', 'wpforo' ), 'spamBlocked' => __( 'Spam Blocked', 'wpforo' ), 'toxicDetected' => __( 'Toxic Detected', 'wpforo' ), 'policyViolations' => __( 'Policy Violations', 'wpforo' ), 'cleanPassed' => __( 'Clean Passed', 'wpforo' ), ), 'featureNames' => wpforo_ai_get_feature_display_names(), 'featureColors' => wpforo_ai_get_feature_colors(), ); ?> $start, 'end' => $end, 'display' => $display, ); } /** * Get feature display names for analytics * * @return array Feature key => Display name mapping */ function wpforo_ai_get_feature_display_names() { return array( 'semantic_search' => __( 'AI Search', 'wpforo' ), 'search_enhance' => __( 'Search Enhancement', 'wpforo' ), 'multi_language_translation' => __( 'Translation', 'wpforo' ), 'topic_summary' => __( 'Topic Summary', 'wpforo' ), 'content_indexing' => __( 'Content Indexing', 'wpforo' ), 'multimodal_image_indexing' => __( 'Image Indexing', 'wpforo' ), 'document_indexing' => __( 'Document Indexing', 'wpforo' ), 'custom_knowledge_indexing' => __( 'Custom Knowledge Indexing', 'wpforo' ), 'ai_topic_generator' => __( 'Topic Generator', 'wpforo' ), 'ai_reply_generator' => __( 'Reply Generator', 'wpforo' ), 'auto_tag_generation' => __( 'Tag Maintenance', 'wpforo' ), 'ai_suggestion' => __( 'AI Suggestions', 'wpforo' ), 'ai_spam_detection' => __( 'Spam Detection', 'wpforo' ), 'ai_toxicity_detection' => __( 'Toxicity Detection', 'wpforo' ), 'ai_rule_compliance' => __( 'Rule Compliance', 'wpforo' ), 'unified_moderation' => __( 'Unified Moderation', 'wpforo' ), 'ai_chat' => __( 'AI Chatbot', 'wpforo' ), 'ai_insight_sentiment' => __( 'Insight: Sentiment', 'wpforo' ), 'ai_insight_trending' => __( 'Insight: Trending', 'wpforo' ), 'ai_insight_recommendations' => __( 'Insight: Recommendations', 'wpforo' ), 'ai_insight_deep_analysis' => __( 'Insight: Deep Analysis', 'wpforo' ), 'ai_insight_sentiment_trend' => __( 'Insight: Sentiment Trend', 'wpforo' ), ); } /** * Get feature colors for charts * * @return array Feature key => Color code mapping */ function wpforo_ai_get_feature_colors() { return array( 'semantic_search' => '#4CAF50', 'search_enhance' => '#8BC34A', 'multi_language_translation' => '#2196F3', 'topic_summary' => '#03A9F4', 'content_indexing' => '#9C27B0', 'multimodal_image_indexing' => '#CE93D8', 'document_indexing' => '#AB47BC', 'custom_knowledge_indexing' => '#7B1FA2', 'ai_topic_generator' => '#FF9800', 'ai_reply_generator' => '#FF5722', 'auto_tag_generation' => '#795548', 'ai_suggestion' => '#00BCD4', 'ai_spam_detection' => '#F44336', 'ai_toxicity_detection' => '#E91E63', 'ai_rule_compliance' => '#673AB7', 'unified_moderation' => '#3F51B5', 'ai_chat' => '#009688', 'ai_insight_sentiment' => '#7C4DFF', 'ai_insight_trending' => '#536DFE', 'ai_insight_recommendations' => '#448AFF', 'ai_insight_deep_analysis' => '#40C4FF', 'ai_insight_sentiment_trend' => '#18FFFF', ); } /** * Render Forum Activity Analytics sub-tab * * This is a FREE feature - all data comes from local wpForo database. * No API calls required. * * @param int $board_id Current board ID */ function wpforo_ai_render_forum_activity_subtab( $board_id ) { // Time range presets $time_ranges = array( 'today' => __( 'Today', 'wpforo' ), '7days' => __( 'Last 7 Days', 'wpforo' ), '30days' => __( 'Last 30 Days', 'wpforo' ), '90days' => __( 'Last 90 Days', 'wpforo' ), 'this_year' => __( 'This Year', 'wpforo' ), 'all_time' => __( 'All Time', 'wpforo' ), 'custom' => __( 'Custom Range', 'wpforo' ), ); $current_range = isset( $_GET['range'] ) ? sanitize_key( $_GET['range'] ) : '7days'; $custom_start = isset( $_GET['start_date'] ) ? sanitize_text_field( $_GET['start_date'] ) : ''; $custom_end = isset( $_GET['end_date'] ) ? sanitize_text_field( $_GET['end_date'] ) : ''; // Calculate time range $time_info = wpforo_ai_calculate_time_range( $current_range, $custom_start, $custom_end ); // Get forum activity data $activity_data = wpforo_ai_get_forum_activity_data( $board_id, $time_info['start'], $time_info['end'] ); ?>
$range_label ) : if ( $range_key === 'custom' ) continue; $range_url = add_query_arg( array( 'page' => 'wpforo-ai', 'tab' => 'analytics', 'subtab' => 'forum_activity', 'boardid' => $board_id, 'range' => $range_key, ), admin_url( 'admin.php' ) ); $active_class = ( $current_range === $range_key ) ? 'active' : ''; ?>

$activity_data['time_series'], 'topForums' => array_slice( $activity_data['forums'], 0, 10 ), 'i18n' => array( 'topics' => __( 'Topics', 'wpforo' ), 'replies' => __( 'Replies', 'wpforo' ), 'posts' => __( 'Posts', 'wpforo' ), ), ); ?> $summary, 'time_series' => $time_series, 'heatmap' => $heatmap, 'forums' => $forums, ); } /** * Get activity summary statistics * * @param string $start_date Start date (Y-m-d H:i:s) * @param string $end_date End date (Y-m-d H:i:s) * @return array Summary stats */ function wpforo_ai_get_activity_summary( $start_date, $end_date ) { // Total new topics $total_topics = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(*) FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND status = 0", $start_date, $end_date ) ); // Total replies (posts that are not first posts) $total_replies = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(*) FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND is_first_post = 0 AND status = 0", $start_date, $end_date ) ); // Active users (distinct users who posted) $active_users = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(DISTINCT userid) FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND userid > 0 AND status = 0", $start_date, $end_date ) ); // Average replies per topic $avg_replies = $total_topics > 0 ? ( $total_replies / $total_topics ) : 0; return array( 'total_topics' => $total_topics, 'total_replies' => $total_replies, 'active_users' => $active_users, 'avg_replies_per_topic' => $avg_replies, ); } /** * Get posts time series (for area chart) * * @param string $start_date Start date * @param string $end_date End date * @return array Time series data */ function wpforo_ai_get_posts_time_series( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT DATE(created) as date, SUM(CASE WHEN is_first_post = 1 THEN 1 ELSE 0 END) as topics, SUM(CASE WHEN is_first_post = 0 THEN 1 ELSE 0 END) as replies FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND status = 0 GROUP BY DATE(created) ORDER BY date ASC", $start_date, $end_date ), ARRAY_A ); // Fill in missing dates with zeros $time_series = array(); $current = strtotime( date( 'Y-m-d', strtotime( $start_date ) ) ); $end = strtotime( date( 'Y-m-d', strtotime( $end_date ) ) ); // Create a lookup for results $data_lookup = array(); foreach ( $results as $row ) { $data_lookup[ $row['date'] ] = $row; } // Fill in all dates while ( $current <= $end ) { $date_str = date( 'Y-m-d', $current ); if ( isset( $data_lookup[ $date_str ] ) ) { $time_series[] = array( 'date' => $date_str, 'topics' => (int) $data_lookup[ $date_str ]['topics'], 'replies' => (int) $data_lookup[ $date_str ]['replies'], ); } else { $time_series[] = array( 'date' => $date_str, 'topics' => 0, 'replies' => 0, ); } $current = strtotime( '+1 day', $current ); } return $time_series; } /** * Get activity heatmap data (day of week vs hour) * * @param string $start_date Start date * @param string $end_date End date * @return array Heatmap data [day][hour] => count */ function wpforo_ai_get_activity_heatmap( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT DAYOFWEEK(created) as day_of_week, HOUR(created) as hour_of_day, COUNT(*) as count FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND status = 0 GROUP BY DAYOFWEEK(created), HOUR(created)", $start_date, $end_date ), ARRAY_A ); // Initialize heatmap (7 days x 24 hours) $heatmap = array(); for ( $day = 1; $day <= 7; $day++ ) { $heatmap[ $day ] = array_fill( 0, 24, 0 ); } // Fill in values $max_count = 0; foreach ( $results as $row ) { $day = (int) $row['day_of_week']; $hour = (int) $row['hour_of_day']; $count = (int) $row['count']; $heatmap[ $day ][ $hour ] = $count; $max_count = max( $max_count, $count ); } return array( 'data' => $heatmap, 'max_count' => $max_count, ); } /** * Get forum activity breakdown * * @param string $start_date Start date * @param string $end_date End date * @return array Forum stats */ function wpforo_ai_get_forum_breakdown( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT f.forumid, f.title, COUNT(DISTINCT CASE WHEN p.is_first_post = 1 THEN p.topicid END) as topics, SUM(CASE WHEN p.is_first_post = 0 THEN 1 ELSE 0 END) as replies, COUNT(DISTINCT p.userid) as active_users, COALESCE(SUM(t.views), 0) as views FROM " . WPF()->tables->forums . " f LEFT JOIN " . WPF()->tables->posts . " p ON f.forumid = p.forumid AND p.created BETWEEN %s AND %s AND p.status = 0 LEFT JOIN " . WPF()->tables->topics . " t ON p.topicid = t.topicid AND p.is_first_post = 1 WHERE f.is_cat = 0 GROUP BY f.forumid, f.title HAVING topics > 0 OR replies > 0 ORDER BY (topics + replies) DESC", $start_date, $end_date ), ARRAY_A ); return $results ?: array(); } /** * Render activity heatmap HTML * * @param array $heatmap_data Heatmap data from wpforo_ai_get_activity_heatmap() */ function wpforo_ai_render_activity_heatmap( $heatmap_data ) { $days = array( 1 => __( 'Sun', 'wpforo' ), 2 => __( 'Mon', 'wpforo' ), 3 => __( 'Tue', 'wpforo' ), 4 => __( 'Wed', 'wpforo' ), 5 => __( 'Thu', 'wpforo' ), 6 => __( 'Fri', 'wpforo' ), 7 => __( 'Sat', 'wpforo' ), ); $data = $heatmap_data['data']; $max_count = max( 1, $heatmap_data['max_count'] ); // Avoid division by zero ?>
$day_name ) : ?>
0 ? ( $count / $max_count ) : 0; $opacity = 0.1 + ( $intensity * 0.9 ); // Range from 0.1 to 1.0 $bg_color = $count > 0 ? "rgba(76, 175, 80, {$opacity})" : 'rgba(0, 0, 0, 0.05)'; ?>
__( 'Today', 'wpforo' ), '7days' => __( 'Last 7 Days', 'wpforo' ), '30days' => __( 'Last 30 Days', 'wpforo' ), '90days' => __( 'Last 90 Days', 'wpforo' ), 'this_year' => __( 'This Year', 'wpforo' ), 'all_time' => __( 'All Time', 'wpforo' ), 'custom' => __( 'Custom Range', 'wpforo' ), ); $current_range = isset( $_GET['range'] ) ? sanitize_key( $_GET['range'] ) : '30days'; $custom_start = isset( $_GET['start_date'] ) ? sanitize_text_field( $_GET['start_date'] ) : ''; $custom_end = isset( $_GET['end_date'] ) ? sanitize_text_field( $_GET['end_date'] ) : ''; // Calculate time range $time_info = wpforo_ai_calculate_time_range( $current_range, $custom_start, $custom_end ); // Get user engagement data $engagement_data = wpforo_ai_get_user_engagement_data( $board_id, $time_info['start'], $time_info['end'] ); ?>
$range_label ) : if ( $range_key === 'custom' ) continue; $range_url = add_query_arg( array( 'page' => 'wpforo-ai', 'tab' => 'analytics', 'subtab' => 'user_engagement', 'boardid' => $board_id, 'range' => $range_key, ), admin_url( 'admin.php' ) ); $active_class = ( $current_range === $range_key ) ? 'active' : ''; ?>
%

#
$contributor ) : $profile_url = WPF()->member->get_profile_url( $contributor['userid'] ); ?>
'wpforo-ai-contributor-avatar' ) ); ?>

$engagement_data['registrations_time_series'], 'distribution' => $engagement_data['activity_distribution'], 'i18n' => array( 'registrations' => __( 'New Registrations', 'wpforo' ), 'powerUsers' => __( 'Power Users (50+ posts)', 'wpforo' ), 'activeUsers' => __( 'Active Users (10-49 posts)', 'wpforo' ), 'occasional' => __( 'Occasional (2-9 posts)', 'wpforo' ), 'oneTime' => __( 'One-time (1 post)', 'wpforo' ), 'lurkers' => __( 'Lurkers (0 posts)', 'wpforo' ), ), ); ?> $summary, 'registrations_time_series' => $registrations, 'top_contributors' => $contributors, 'activity_distribution' => $distribution, 'recent_registrations' => $recent, ); } /** * Get engagement summary statistics * * @param string $start_date Start date * @param string $end_date End date * @return array Summary stats */ function wpforo_ai_get_engagement_summary( $start_date, $end_date ) { global $wpdb; // New members registered in period $new_members = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->users} WHERE user_registered BETWEEN %s AND %s", $start_date, $end_date ) ); // Active members (users who posted in period) $active_members = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(DISTINCT userid) FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND userid > 0 AND status = 0", $start_date, $end_date ) ); // Total registered users $total_users = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" ); // Engagement rate (active / total) $engagement_rate = $total_users > 0 ? ( $active_members / $total_users ) * 100 : 0; // Top contributor $top_contributor = WPF()->db->get_row( WPF()->db->prepare( "SELECT p.userid, u.display_name, COUNT(*) as posts FROM " . WPF()->tables->posts . " p JOIN {$wpdb->users} u ON p.userid = u.ID WHERE p.created BETWEEN %s AND %s AND p.userid > 0 AND p.status = 0 GROUP BY p.userid ORDER BY posts DESC LIMIT 1", $start_date, $end_date ), ARRAY_A ); return array( 'new_members' => $new_members, 'active_members' => $active_members, 'engagement_rate' => $engagement_rate, 'top_contributor_name' => $top_contributor ? $top_contributor['display_name'] : '', 'top_contributor_posts' => $top_contributor ? (int) $top_contributor['posts'] : 0, ); } /** * Get registrations time series * * @param string $start_date Start date * @param string $end_date End date * @return array Time series data */ function wpforo_ai_get_registrations_time_series( $start_date, $end_date ) { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(user_registered) as date, COUNT(*) as count FROM {$wpdb->users} WHERE user_registered BETWEEN %s AND %s GROUP BY DATE(user_registered) ORDER BY date ASC", $start_date, $end_date ), ARRAY_A ); // Fill in missing dates with zeros $time_series = array(); $current = strtotime( date( 'Y-m-d', strtotime( $start_date ) ) ); $end = strtotime( date( 'Y-m-d', strtotime( $end_date ) ) ); $data_lookup = array(); foreach ( $results as $row ) { $data_lookup[ $row['date'] ] = (int) $row['count']; } while ( $current <= $end ) { $date_str = date( 'Y-m-d', $current ); $time_series[] = array( 'date' => $date_str, 'count' => isset( $data_lookup[ $date_str ] ) ? $data_lookup[ $date_str ] : 0, ); $current = strtotime( '+1 day', $current ); } return $time_series; } /** * Get top contributors * * @param string $start_date Start date * @param string $end_date End date * @return array Top contributors */ function wpforo_ai_get_top_contributors( $start_date, $end_date ) { global $wpdb; $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT p.userid, u.display_name, SUM(CASE WHEN p.is_first_post = 1 THEN 1 ELSE 0 END) as topics, SUM(CASE WHEN p.is_first_post = 0 THEN 1 ELSE 0 END) as replies, COUNT(*) as total_posts, pr.groupid FROM " . WPF()->tables->posts . " p JOIN {$wpdb->users} u ON p.userid = u.ID LEFT JOIN " . WPF()->tables->profiles . " pr ON p.userid = pr.userid WHERE p.created BETWEEN %s AND %s AND p.userid > 0 AND p.status = 0 GROUP BY p.userid, u.display_name, pr.groupid ORDER BY total_posts DESC LIMIT 10", $start_date, $end_date ), ARRAY_A ); // Add usergroup names if ( $results ) { foreach ( $results as &$row ) { $groupid = (int) $row['groupid']; $usergroup = WPF()->usergroup->get_usergroup( $groupid ); $row['usergroup_name'] = $usergroup ? $usergroup['name'] : ''; $row['usergroup_color'] = $usergroup && ! empty( $usergroup['color'] ) ? $usergroup['color'] : '#666'; } } return $results ?: array(); } /** * Get user activity distribution * * @param string $start_date Start date * @param string $end_date End date * @return array Distribution data */ function wpforo_ai_get_user_activity_distribution( $start_date, $end_date ) { global $wpdb; // Get post counts for all users $user_posts = WPF()->db->get_results( WPF()->db->prepare( "SELECT userid, COUNT(*) as posts FROM " . WPF()->tables->posts . " WHERE created BETWEEN %s AND %s AND userid > 0 AND status = 0 GROUP BY userid", $start_date, $end_date ), ARRAY_A ); // Count users in each category $power_users = 0; // 50+ posts $active_users = 0; // 10-49 posts $occasional = 0; // 2-9 posts $one_time = 0; // 1 post foreach ( $user_posts as $user ) { $posts = (int) $user['posts']; if ( $posts >= 50 ) { $power_users++; } elseif ( $posts >= 10 ) { $active_users++; } elseif ( $posts >= 2 ) { $occasional++; } else { $one_time++; } } // Count lurkers (users registered in period who haven't posted) $registered_users = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->users} WHERE user_registered BETWEEN %s AND %s", $start_date, $end_date ) ); $posting_users = count( $user_posts ); $lurkers = max( 0, (int) $registered_users - $posting_users ); return array( 'power_users' => $power_users, 'active_users' => $active_users, 'occasional' => $occasional, 'one_time' => $one_time, 'lurkers' => $lurkers, ); } /** * Get recent registrations with post info * * @param string $start_date Start date * @param string $end_date End date * @return array Recent registrations */ function wpforo_ai_get_recent_registrations( $start_date, $end_date ) { global $wpdb; $users = $wpdb->get_results( $wpdb->prepare( "SELECT ID, display_name, user_registered FROM {$wpdb->users} WHERE user_registered BETWEEN %s AND %s ORDER BY user_registered DESC LIMIT 20", $start_date, $end_date ), ARRAY_A ); $result = array(); foreach ( $users as $user ) { // Get post count and first post date for this user $post_info = WPF()->db->get_row( WPF()->db->prepare( "SELECT COUNT(*) as posts, MIN(created) as first_post FROM " . WPF()->tables->posts . " WHERE userid = %d AND status = 0", $user['ID'] ), ARRAY_A ); $result[] = array( 'userid' => $user['ID'], 'display_name' => $user['display_name'], 'registered_date' => wpforo_ai_format_date( $user['user_registered'] ), 'posts' => $post_info ? (int) $post_info['posts'] : 0, 'first_post_date' => ( $post_info && $post_info['first_post'] ) ? wpforo_ai_format_date( $post_info['first_post'] ) : null, ); } return $result; } /** * Render Content Performance Analytics sub-tab * * FREE feature - all data comes from local wpForo database. * * @param int $board_id Current board ID */ function wpforo_ai_render_content_performance_subtab( $board_id ) { // Time range presets $time_ranges = array( 'today' => __( 'Today', 'wpforo' ), '7days' => __( 'Last 7 Days', 'wpforo' ), '30days' => __( 'Last 30 Days', 'wpforo' ), '90days' => __( 'Last 90 Days', 'wpforo' ), 'this_year' => __( 'This Year', 'wpforo' ), 'all_time' => __( 'All Time', 'wpforo' ), 'custom' => __( 'Custom Range', 'wpforo' ), ); $current_range = isset( $_GET['range'] ) ? sanitize_key( $_GET['range'] ) : '30days'; $custom_start = isset( $_GET['start_date'] ) ? sanitize_text_field( $_GET['start_date'] ) : ''; $custom_end = isset( $_GET['end_date'] ) ? sanitize_text_field( $_GET['end_date'] ) : ''; // Calculate time range $time_info = wpforo_ai_calculate_time_range( $current_range, $custom_start, $custom_end ); // Get content performance data $content_data = wpforo_ai_get_content_performance_data( $board_id, $time_info['start'], $time_info['end'] ); ?>
$range_label ) : if ( $range_key === 'custom' ) continue; $range_url = add_query_arg( array( 'page' => 'wpforo-ai', 'tab' => 'analytics', 'subtab' => 'content', 'boardid' => $board_id, 'range' => $range_key, ), admin_url( 'admin.php' ) ); $active_class = ( $current_range === $range_key ) ? 'active' : ''; ?>
%

#
$topic ) : $topic_url = WPF()->topic->get_url( $topic['topicid'] ); ?>

#
$topic ) : $topic_url = WPF()->topic->get_url( $topic['topicid'] ); ?>

'tag', 'wpfs' => $tag['tag'], ), $forum_url ); ?>
$content_data['forum_distribution'], 'i18n' => array( 'topics' => __( 'Topics', 'wpforo' ), ), ); ?> $summary, 'most_viewed' => $most_viewed, 'most_discussed' => $most_discussed, 'unanswered' => $unanswered, 'forum_distribution' => $forum_distribution, 'popular_tags' => $popular_tags, ); } /** * Get content summary statistics * * @param string $start_date Start date * @param string $end_date End date * @return array Summary stats */ function wpforo_ai_get_content_summary( $start_date, $end_date ) { // Total views of topics created in period $total_views = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COALESCE(SUM(views), 0) FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND status = 0", $start_date, $end_date ) ); // Total topics created $total_topics = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(*) FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND status = 0", $start_date, $end_date ) ); // Unanswered topics (created in period with only 1 post - the original) $unanswered_count = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(*) FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND posts = 1 AND status = 0", $start_date, $end_date ) ); // Solved topics (if solved field exists) $solved_count = (int) WPF()->db->get_var( WPF()->db->prepare( "SELECT COUNT(*) FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND solved = 1 AND status = 0", $start_date, $end_date ) ); // Calculate solved rate $solved_rate = $total_topics > 0 ? ( $solved_count / $total_topics ) * 100 : 0; return array( 'total_views' => $total_views, 'total_topics' => $total_topics, 'unanswered_count' => $unanswered_count, 'solved_rate' => $solved_rate, ); } /** * Get most viewed topics * * @param string $start_date Start date * @param string $end_date End date * @return array Most viewed topics */ function wpforo_ai_get_most_viewed_topics( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT t.topicid, t.title, t.views, (t.posts - 1) as replies, f.title as forum_title FROM " . WPF()->tables->topics . " t LEFT JOIN " . WPF()->tables->forums . " f ON t.forumid = f.forumid WHERE t.created BETWEEN %s AND %s AND t.status = 0 ORDER BY t.views DESC LIMIT 10", $start_date, $end_date ), ARRAY_A ); return $results ?: array(); } /** * Get most discussed topics * * @param string $start_date Start date * @param string $end_date End date * @return array Most discussed topics */ function wpforo_ai_get_most_discussed_topics( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT t.topicid, t.title, t.views, (t.posts - 1) as replies, f.title as forum_title FROM " . WPF()->tables->topics . " t LEFT JOIN " . WPF()->tables->forums . " f ON t.forumid = f.forumid WHERE t.created BETWEEN %s AND %s AND t.status = 0 ORDER BY t.posts DESC LIMIT 10", $start_date, $end_date ), ARRAY_A ); return $results ?: array(); } /** * Get unanswered topics * * @param string $start_date Start date * @param string $end_date End date * @return array Unanswered topics */ function wpforo_ai_get_unanswered_topics( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT t.topicid, t.title, t.created, f.title as forum_title FROM " . WPF()->tables->topics . " t LEFT JOIN " . WPF()->tables->forums . " f ON t.forumid = f.forumid WHERE t.created BETWEEN %s AND %s AND t.posts = 1 AND t.status = 0 ORDER BY t.created DESC LIMIT 15", $start_date, $end_date ), ARRAY_A ); return $results ?: array(); } /** * Get forum distribution for pie chart * * @param string $start_date Start date * @param string $end_date End date * @return array Forum distribution */ function wpforo_ai_get_forum_distribution( $start_date, $end_date ) { $results = WPF()->db->get_results( WPF()->db->prepare( "SELECT f.forumid, f.title, COUNT(t.topicid) as topics FROM " . WPF()->tables->forums . " f LEFT JOIN " . WPF()->tables->topics . " t ON f.forumid = t.forumid AND t.created BETWEEN %s AND %s AND t.status = 0 WHERE f.is_cat = 0 GROUP BY f.forumid, f.title HAVING topics > 0 ORDER BY topics DESC LIMIT 8", $start_date, $end_date ), ARRAY_A ); return $results ?: array(); } /** * Get popular tags * * @param string $start_date Start date * @param string $end_date End date * @return array Popular tags */ function wpforo_ai_get_popular_tags( $start_date, $end_date ) { // Get topics with tags in the period $topics_with_tags = WPF()->db->get_results( WPF()->db->prepare( "SELECT tags FROM " . WPF()->tables->topics . " WHERE created BETWEEN %s AND %s AND tags IS NOT NULL AND tags != '' AND status = 0", $start_date, $end_date ), ARRAY_A ); // Count tag occurrences $tag_counts = array(); foreach ( $topics_with_tags as $topic ) { $tags = maybe_unserialize( $topic['tags'] ); if ( is_array( $tags ) ) { foreach ( $tags as $tag ) { $tag = trim( $tag ); if ( $tag ) { if ( ! isset( $tag_counts[ $tag ] ) ) { $tag_counts[ $tag ] = 0; } $tag_counts[ $tag ]++; } } } elseif ( is_string( $tags ) && $tags ) { // Handle comma-separated tags $tag_array = explode( ',', $tags ); foreach ( $tag_array as $tag ) { $tag = trim( $tag ); if ( $tag ) { if ( ! isset( $tag_counts[ $tag ] ) ) { $tag_counts[ $tag ] = 0; } $tag_counts[ $tag ]++; } } } } // Sort by count and limit arsort( $tag_counts ); $tag_counts = array_slice( $tag_counts, 0, 30, true ); // Format for output $result = array(); foreach ( $tag_counts as $tag => $count ) { $result[] = array( 'tag' => $tag, 'count' => $count, ); } return $result; } /** * Render AI-Powered Insights sub-tab * * Premium features that use credits for AI analysis: * - Sentiment Analysis: Analyze community mood from recent posts * - Trending Topics: AI-detected emerging discussions * - Growth Recommendations: AI-generated actionable suggestions * * @param int $board_id Current board ID * @param array $status Tenant status data */ function wpforo_ai_render_ai_insights_subtab( $board_id, $status ) { // Get available credits $subscription = isset( $status['subscription'] ) && is_array( $status['subscription'] ) ? $status['subscription'] : []; $credits_remaining = isset( $subscription['credits_remaining'] ) ? (int) $subscription['credits_remaining'] : 0; $credits_total = isset( $subscription['credits_total'] ) ? (int) $subscription['credits_total'] : 0; // Define insight types with their credit costs // Top row: 2 column grid (half width each) $insight_types_top = array( 'sentiment' => array( 'label' => __( 'Community Sentiment', 'wpforo' ), 'description' => __( 'Analyze mood with different emotion categories: happy, excited, frustrated, angry...', 'wpforo' ), 'period' => __( 'Based on last 200 posts', 'wpforo' ), 'credits' => 2, 'icon' => 'chart-pie', ), 'trending' => array( 'label' => __( 'Trending Topics', 'wpforo' ), 'description' => __( 'Discover emerging discussions and hot topics in your forum using AI pattern detection.', 'wpforo' ), 'period' => __( 'Last 30 days', 'wpforo' ), 'credits' => 1, 'icon' => 'chart-line', ), ); // Full-width sections below $insight_types_full = array( 'deep_analysis' => array( 'label' => __( 'Community Deep Dive', 'wpforo' ), 'description' => __( 'Comprehensive analysis of user engagement patterns, content metrics, keyword sentiment hotspots, top contributors mood, and activity patterns.', 'wpforo' ), 'period' => __( 'Last 30 days', 'wpforo' ), 'credits' => 5, 'icon' => 'analytics', 'full_width' => true, ), 'sentiment_trend' => array( 'label' => __( 'Sentiment Trend Over Time', 'wpforo' ), 'description' => __( 'Track how community mood has changed over time with visualization of sentiment shifts, key events, and mood forecasting.', 'wpforo' ), 'period' => __( 'Last 30 days', 'wpforo' ), 'credits' => 4, 'icon' => 'chart-area', 'full_width' => true, ), 'recommendations' => array( 'label' => __( 'Growth Recommendations', 'wpforo' ), 'description' => __( 'Get AI-generated actionable moderation suggestions based on your forum\'s data. Available once per day.', 'wpforo' ), 'period' => __( 'All-time statistics with recent activity', 'wpforo' ), 'credits' => 1, 'icon' => 'lightbulb', 'full_width' => true, 'daily_limit' => true, ), ); // Combined for cache lookup $insight_types = array_merge( $insight_types_top, $insight_types_full ); // Check for cached results $cached_insights = wpforo_ai_get_cached_insights( $board_id ); ?>
$insight ) : $cached_data = isset( $cached_insights[ $type ] ) ? $cached_insights[ $type ] : null; $has_cached = ! empty( $cached_data ) && isset( $cached_data['data'] ); $cached_time = $has_cached && isset( $cached_data['timestamp'] ) ? human_time_diff( $cached_data['timestamp'], time() ) : ''; $is_outdated = $has_cached && isset( $cached_data['timestamp'] ) && wpforo_ai_is_insight_outdated( $cached_data['timestamp'] ); ?>

$insight ) : $cached_data = isset( $cached_insights[ $type ] ) ? $cached_insights[ $type ] : null; $has_cached = ! empty( $cached_data ) && isset( $cached_data['data'] ); $cached_time = $has_cached && isset( $cached_data['timestamp'] ) ? human_time_diff( $cached_data['timestamp'], time() ) : ''; $is_outdated = $has_cached && isset( $cached_data['timestamp'] ) && wpforo_ai_is_insight_outdated( $cached_data['timestamp'] ); // Check for daily limit $has_daily_limit = isset( $insight['daily_limit'] ) && $insight['daily_limit']; $used_today = false; if ( $has_daily_limit && $has_cached && isset( $cached_data['timestamp'] ) ) { // Check if cached result is from today $cached_date = date( 'Y-m-d', $cached_data['timestamp'] ); $today_date = date( 'Y-m-d', current_time( 'timestamp' ) ); $used_today = ( $cached_date === $today_date ); } ?>

admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpforo_ai_insights_nonce' ), 'boardId' => $board_id, 'creditsRemaining' => $credits_remaining, 'i18n' => array( 'confirmTitle' => __( 'Run AI Analysis', 'wpforo' ), 'confirmMessage' => __( 'This will use %d credit(s) from your balance.', 'wpforo' ), 'confirmButton' => __( 'Run Analysis', 'wpforo' ), 'cancelButton' => __( 'Cancel', 'wpforo' ), 'analyzing' => __( 'Analyzing...', 'wpforo' ), 'error' => __( 'An error occurred. Please try again.', 'wpforo' ), 'insufficientCredits' => __( 'Insufficient credits to run this analysis.', 'wpforo' ), 'cachedJustNow' => __( 'Just now', 'wpforo' ), ), ); ?> array( 'emoji' => '😊', 'label' => __( 'Happy', 'wpforo' ), 'color' => '#4CAF50' ), 'excited' => array( 'emoji' => '🎉', 'label' => __( 'Excited', 'wpforo' ), 'color' => '#8BC34A' ), 'neutral' => array( 'emoji' => '😐', 'label' => __( 'Neutral', 'wpforo' ), 'color' => '#9E9E9E' ), 'confused' => array( 'emoji' => '🤔', 'label' => __( 'Confused', 'wpforo' ), 'color' => '#FF9800' ), 'frustrated' => array( 'emoji' => '😤', 'label' => __( 'Frustrated', 'wpforo' ), 'color' => '#FF5722' ), 'angry' => array( 'emoji' => '😠', 'label' => __( 'Angry', 'wpforo' ), 'color' => '#F44336' ), 'sad' => array( 'emoji' => '😢', 'label' => __( 'Sad', 'wpforo' ), 'color' => '#2196F3' ), ); // Backwards compatibility: if old format (positive/neutral/negative), convert if ( isset( $data['positive'] ) && ! isset( $data['happy'] ) ) { $data['happy'] = isset( $data['positive'] ) ? (int) $data['positive'] : 0; $data['neutral'] = isset( $data['neutral'] ) ? (int) $data['neutral'] : 0; $data['angry'] = isset( $data['negative'] ) ? (int) $data['negative'] : 0; $data['excited'] = 0; $data['confused'] = 0; $data['frustrated'] = 0; $data['sad'] = 0; } $summary = isset( $data['summary'] ) ? sanitize_text_field( $data['summary'] ) : ''; ?>
$emotion ) : $value = isset( $data[ $key ] ) ? (int) $data[ $key ] : 0; if ( $value > 0 ) : // Only show emotions with values > 0 ?>
%

%
h
()

%

'#4CAF50', 'negative' => '#F44336', 'mixed' => '#FF9800', 'neutral' => '#9E9E9E', ); $color = isset( $sentiment_colors[ $sentiment ] ) ? $sentiment_colors[ $sentiment ] : '#9E9E9E'; ?>

😊
%
😐
%
😟
%

array( 'icon' => '', 'color' => '#4CAF50', 'label' => __( 'Improving', 'wpforo' ), ), 'declining' => array( 'icon' => '', 'color' => '#F44336', 'label' => __( 'Declining', 'wpforo' ), ), 'stable' => array( 'icon' => '', 'color' => '#9E9E9E', 'label' => __( 'Stable', 'wpforo' ), ), 'volatile' => array( 'icon' => '', 'color' => '#FF9800', 'label' => __( 'Volatile', 'wpforo' ), ), ); $trend_info = isset( $trend_icons[ $trend_direction ] ) ? $trend_icons[ $trend_direction ] : $trend_icons['stable']; ?>
/10
😊
😟
🔥

()

'#4CAF50', 'excited' => '#8BC34A', 'neutral' => '#9E9E9E', 'confused' => '#FF9800', 'frustrated' => '#FF5722', 'angry' => '#F44336', 'sad' => '#2196F3', ); $emotion_emojis = array( 'happy' => '😊', 'excited' => '🎉', 'neutral' => '😐', 'confused' => '🤔', 'frustrated' => '😤', 'angry' => '😠', 'sad' => '😢', ); ?>
0
array( 'emoji' => '😊', 'label' => __( 'Happy', 'wpforo' ), 'color' => '#4CAF50' ), 'excited' => array( 'emoji' => '🎉', 'label' => __( 'Excited', 'wpforo' ), 'color' => '#8BC34A' ), 'neutral' => array( 'emoji' => '😐', 'label' => __( 'Neutral', 'wpforo' ), 'color' => '#9E9E9E' ), 'confused' => array( 'emoji' => '🤔', 'label' => __( 'Confused', 'wpforo' ), 'color' => '#FF9800' ), 'frustrated' => array( 'emoji' => '😤', 'label' => __( 'Frustrated', 'wpforo' ), 'color' => '#FF5722' ), 'angry' => array( 'emoji' => '😠', 'label' => __( 'Angry', 'wpforo' ), 'color' => '#F44336' ), 'sad' => array( 'emoji' => '😢', 'label' => __( 'Sad', 'wpforo' ), 'color' => '#2196F3' ), ); foreach ( $legend_emotions as $key => $emotion ) : ?>

→

$data, 'timestamp' => time(), ); // Use update_option for permanent storage (survives cache clears) update_option( $option_key, $insight_data, false ); // autoload = false for performance } /** * Check if cached insight data is outdated (older than 15 days) * * @param int $timestamp Unix timestamp when data was cached * @return bool True if data is older than 15 days */ function wpforo_ai_is_insight_outdated( $timestamp ) { $days_old = ( time() - $timestamp ) / DAY_IN_SECONDS; return $days_old >= 15; } /** * Delete cached AI insight for a specific type and board * * @param int $board_id Board ID * @param string $type Insight type (or 'all' to delete all) */ function wpforo_ai_delete_cached_insight( $board_id, $type = 'all' ) { if ( 'all' === $type ) { $insight_types = array( 'sentiment', 'trending', 'deep_analysis', 'sentiment_trend', 'recommendations' ); foreach ( $insight_types as $t ) { $option_key = 'wpforo_ai_insight_' . $t . '_' . intval( $board_id ); delete_option( $option_key ); } } else { $option_key = 'wpforo_ai_insight_' . sanitize_key( $type ) . '_' . intval( $board_id ); delete_option( $option_key ); } }