render_header( __('Overview', 'answerrails'), __('Dashboard and analytics for your AI chat assistant.', 'answerrails') ); // #174 (D10, the Overview tidy-up): the free Overview is // health/status facts + volume counts + the #140 C1 grounding-outcomes // card. The analytics summary (BI tiles, structured fallbacks, top // sources, quality distribution) is Pro content rendered through the E9 // insertion points (`answerrails_overview_sections`); ideas, knowledge // gaps and invalid-URL monitoring live in the Business Intelligence // admin, not here, and this screen has no recent-conversations detail // section at all (Chat Logs is that surface). Content is simply present // or absent. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display parameter; no state change $raw_time_range = isset($_GET['range']) ? sanitize_text_field(wp_unslash($_GET['range'])) : '24h'; $time_ranges = array( '24h' => array( 'label' => __('Last 24 Hours', 'answerrails'), 'period' => '24_hours', 'start' => '-24 hours', ), '7d' => array( 'label' => __('Last 7 Days', 'answerrails'), 'period' => '7_days', 'start' => '-7 days', ), '30d' => array( 'label' => __('Last 30 Days', 'answerrails'), 'period' => '30_days', 'start' => '-30 days', ), '90d' => array( 'label' => __('Last 90 Days', 'answerrails'), 'period' => '90_days', 'start' => '-90 days', ), 'all' => array( 'label' => __('All Time', 'answerrails'), 'period' => 'all_time', 'start' => null, ), ); $time_range = array_key_exists($raw_time_range, $time_ranges) ? $raw_time_range : '24h'; $range = $time_ranges[$time_range]; $end_timestamp = current_time('timestamp'); $start_timestamp = null; if (!empty($range['start'])) { $start_timestamp = strtotime($range['start'], $end_timestamp); } $range_label = $range['label']; $rag_period = $range['period']; $date_query = array(); if ($start_timestamp) { $date_query[] = array( 'after' => gmdate('Y-m-d H:i:s', $start_timestamp), 'before' => gmdate('Y-m-d H:i:s', $end_timestamp), 'inclusive' => true, ); } $total_conversations = 0; $total_messages = 0; $chatlog_ids = array(); $sampled_conversations = 0; $chatlog_limit = (int) apply_filters('answerrails_overview_chatlog_limit', 500); if ($chatlog_limit <= 0) { $chatlog_limit = 500; } if (post_type_exists('answerrails_chatlog')) { $metrics_args = array( 'post_type' => 'answerrails_chatlog', 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'posts_per_page' => $chatlog_limit, 'paged' => 1, 'no_found_rows' => false, 'fields' => 'ids', ); if (!empty($date_query)) { $metrics_args['date_query'] = $date_query; } $metrics_query = new WP_Query($metrics_args); $total_conversations = (int) $metrics_query->found_posts; $chatlog_ids = array_map('intval', $metrics_query->posts); $sampled_conversations = count($chatlog_ids); wp_reset_postdata(); // #174: this scan counts VOLUME only — no per-conversation quality or // deflection aggregation, and no detail collection. Chat Logs is the // detail surface, and the quality work is the E9 analytics summary's // business (its consumer computes from the chatlog ids passed in the // E9 context). foreach ($chatlog_ids as $post_id) { $content = get_post_field('post_content', $post_id); $conversation = json_decode($content, true); if (is_array($conversation) && !empty($conversation)) { $total_messages += count($conversation); } } } // The Performance Metrics "Response Time" row's series: chat-log turn // response times over the 50 newest logs. (#377 D4: the success figure is // no longer computed here — it is the Chat card's, read below.) $response_times = array(); if (post_type_exists('answerrails_chatlog')) { $perf_args = array( 'post_type' => 'answerrails_chatlog', 'post_status' => 'publish', 'posts_per_page' => 50, 'orderby' => 'date', 'order' => 'DESC', ); $perf_query = new WP_Query($perf_args); if ($perf_query->have_posts()) { while ($perf_query->have_posts()) { $perf_query->the_post(); $post_id = get_the_ID(); $content = get_post_field('post_content', $post_id); $turns = json_decode($content, true); if (is_array($turns)) { foreach ($turns as $turn) { if (isset($turn['metadata']['response_time'])) { $response_times[] = $turn['metadata']['response_time'] / 1000; } } } } wp_reset_postdata(); } } $avg_response_time = !empty($response_times) ? array_sum($response_times) / count($response_times) : null; // #377 D4: the API Success Rate tile and its Performance Metrics row read // the Chat card's own figure and status — the same snapshot the health // widget renders (cached per window), never a second series. The tile // stays: it is the free list's only built-in row, and the // experience_scores extension point renders into this list. $ar_chat_health = class_exists('AnswerRails_API_Health_Monitor') ? ((new AnswerRails_API_Health_Monitor())->get_dashboard_snapshot(AnswerRails_Utils::get_active_provider(), $this->range_window($time_range))['chat'] ?? array()) : array('status' => 'unknown'); $ar_chat_success = AnswerRails_Admin::chat_success_summary($ar_chat_health); // The free Experience Scores list carries the OPERATIONAL summary only // (API success). The business summaries (quality, deflection) are the E9 // analytics summary's content — a consumer appends rows at the // experience_scores insertion point below. $status_summaries = array(); $status_summaries[] = array( 'label' => __('API Success Rate', 'answerrails'), 'description' => __('Successful chat calls over the selected range — the Chat card\'s figure.', 'answerrails'), 'value' => $ar_chat_success['value'], 'unit' => '%', 'precision' => 0, 'percent' => null !== $ar_chat_success['value'] ? max(0, min(100, (float) $ar_chat_success['value'])) : 0, 'state' => $ar_chat_success['state'], ); $api_key = AnswerRails_Utils::get_api_key(); $chat_enabled = get_option('answerrails_chat_enabled', '0'); $system_status = (!empty($api_key) && '1' === $chat_enabled); // The grounding-outcomes card metrics (#140 C1). This is the ONLY // assembly path here since the tidy-up — the old can('analytics') branch // (get_analytics + detect_knowledge_gaps) left with the BI surfaces. // Per-turn metric RECORDING is unaffected and always runs. $overview_metrics = array(); if (class_exists('AnswerRails_RAG_Analytics')) { $overview_metrics = (new AnswerRails_RAG_Analytics())->get_overview_metrics($rag_period); } // E9 (#174, `answerrails_overview_sections`): the context handed to // every insertion point below — the range, the sampled conversation ids, // and the free-card metrics, so the pro analytics summary can compute // without re-deriving the scan. $ar_overview_context = array( 'range' => $time_range, 'range_label' => $range_label, 'period' => $rag_period, 'chatlog_ids' => $chatlog_ids, 'total_conversations' => $total_conversations, 'sampled_conversations' => $sampled_conversations, 'overview_metrics' => $overview_metrics, ); // The overview nonce left with overview-page.js (#174): its two // consumers (conversation-detail toggle, invalid-URL clear) left the page. ?>
$config): ?>

render_api_health_widget($time_range); ?>

0; ?>

0): ?>

'analytics_tiles'))); ?> 'analytics_detail'))); ?>

'experience_scores'))); ?>

—
—
publish) ? esc_html(number_format_i18n($knowledge_count->publish)) : esc_html('0'); } else { esc_html_e('N/A', 'answerrails'); } ?> publish)) { echo '' . esc_html__('Active', 'answerrails') . ''; } else { echo '' . esc_html__('Empty', 'answerrails') . ''; } } else { echo '—'; } ?>

'quality_distribution'))); ?>
render_footer();