__('AI Chat Analytics', 'ai-eshop-optimizer'), 'description' => __('See exactly what your shoppers asked the AI assistant and how it answered, how happy they were, which pages drive chats, where the catalogue had gaps, and whether those chats led to orders.', 'ai-eshop-optimizer'), 'bullets' => array( __('Full, searchable conversation log with happiness ratings', 'ai-eshop-optimizer'), __('Volume + happiness trends, logged-in vs guest', 'ai-eshop-optimizer'), __('Which pages shoppers start chats from (via the Session Tracker)', 'ai-eshop-optimizer'), __('Catalogue gaps — questions that surfaced no products', 'ai-eshop-optimizer'), __('Chat-assisted conversions + revenue', 'ai-eshop-optimizer'), ), ); } public function register_hooks() { if (!class_exists('AIEO_Chat_Analytics') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/dmm/chat-analytics/class-aieo-chat-analytics.php')) { require_once AIEO_DIR . 'includes/dmm/chat-analytics/class-aieo-chat-analytics.php'; } if (class_exists('AIEO_Chat_Analytics')) { add_action(AIEO_Chat_Analytics::STATS_REFRESH_HOOK, array('AIEO_Chat_Analytics', 'refresh_stats_cache')); } add_action('wp_ajax_aieo_chat_analytics_refresh_stats', array($this, 'ajax_refresh_stats')); add_action('wp_ajax_aieo_chat_analytics_stats_cadence', array($this, 'ajax_save_stats_cadence')); add_action('wp_ajax_aieo_chat_analytics_save_retention', array($this, 'ajax_save_retention')); add_filter('cron_schedules', array($this, 'ensure_cron_schedules')); add_action('admin_init', array($this, 'maybe_schedule_stats')); add_action('admin_init', array($this, 'ensure_chat_schema')); } public function ensure_chat_schema() { if (class_exists('AI_eShop_Chat_Database_Manager') && method_exists('AI_eShop_Chat_Database_Manager', 'ensure_chat_history_columns')) { AI_eShop_Chat_Database_Manager::ensure_chat_history_columns(); } } public function enqueue_admin_assets() { $nonce = wp_create_nonce(self::NONCE); wp_register_script('aieo-cac-admin', false, array('jquery'), defined('AIEO_VERSION') ? AIEO_VERSION : '1', true); wp_enqueue_script('aieo-cac-admin'); $js = 'jQuery(function($){var n="' . esc_js($nonce) . '",u=window.ajaxurl,job=document.getElementById("aieo-cac-refresh-job");' . 'function st(o){var s=window.roosterAdmin&&window.roosterAdmin.states;if(s&&job){s.job(job,o);}}' . 'function refresh(){st({state:"running",value:0});$.post(u,{action:"aieo_chat_analytics_refresh_stats",nonce:n},function(r){if(r&&r.success){location.reload();}else{st({state:"failed",error:(r&&r.data&&r.data.message)||""});}}).fail(function(){st({state:"failed",error:""});});}' . '$("#aieo-cac-refresh").on("click",refresh);' . 'document.addEventListener("rooster-admin:save",function(e){var c=e.target;if(!c||"aieo-cac-cadence"!==c.id||!e.detail||"function"!==typeof e.detail.respondWith){return;}' . 'e.detail.respondWith(function(){return fetch(u,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:e.detail.body.toString()}).then(function(r){return r.json();}).then(function(j){if(!j||!j.success){throw new Error((j&&j.data&&j.data.message)||"");}return {saved:true};});});});' . 'if(job&&"running"===job.getAttribute("data-ra-job")){refresh();}' . '});'; wp_add_inline_script('aieo-cac-admin', $js); } private static function get_settings() { $s = get_option(self::SETTINGS_OPTION, array()); return is_array($s) ? $s : array(); } public static function get_stats_cadence() { $s = self::get_settings(); $c = isset($s['stats_refresh']) ? (string) $s['stats_refresh'] : 'daily'; return in_array($c, self::STATS_CADENCES, true) ? $c : 'daily'; } public function ensure_cron_schedules($schedules) { if (!is_array($schedules)) { $schedules = array(); } if (!isset($schedules['weekly'])) { $schedules['weekly'] = array('interval' => 7 * DAY_IN_SECONDS, 'display' => __('Once Weekly', 'ai-eshop-optimizer')); } if (!isset($schedules['monthly'])) { $schedules['monthly'] = array('interval' => 30 * DAY_IN_SECONDS, 'display' => __('Once Monthly', 'ai-eshop-optimizer')); } return $schedules; } public function maybe_schedule_stats() { if (!class_exists('AIEO_Chat_Analytics')) { return; } $hook = AIEO_Chat_Analytics::STATS_REFRESH_HOOK; $current = wp_get_schedule($hook); $cadence = self::get_stats_cadence(); if ('off' === $cadence || !$this->table_exists($this->chat_table())) { if (false !== $current) { wp_clear_scheduled_hook($hook); } return; } if ($current === $cadence) { return; } if (false !== $current) { wp_clear_scheduled_hook($hook); } wp_schedule_event(time() + 300, $cadence, $hook); } public function ajax_refresh_stats() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts') || !class_exists('AIEO_Chat_Analytics')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $payload = AIEO_Chat_Analytics::refresh_stats_cache(); wp_send_json_success(array('generated_at' => $payload['generated_at'])); } public function ajax_save_stats_cadence() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $cadence = isset($_POST['cadence']) ? sanitize_key(wp_unslash($_POST['cadence'])) : 'daily'; if (!in_array($cadence, self::STATS_CADENCES, true)) { $cadence = 'daily'; } $s = self::get_settings(); $s['stats_refresh'] = $cadence; update_option(self::SETTINGS_OPTION, $s, false); $this->maybe_schedule_stats(); wp_send_json_success(array('cadence' => $cadence)); } public function ajax_save_retention() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $days = isset($_POST['days']) ? (int) wp_unslash($_POST['days']) : 0; $days = max(1, min(3650, $days)); update_option('aieo_chat_retention_days', $days, false); wp_send_json_success(array('days' => $days)); } public function get_tab_arg() { return 'cac_section'; } public function get_tabs() { return array( 'overview' => array('label' => __('Overview', 'ai-eshop-optimizer'), 'icon' => 'layout-dashboard'), 'conversations' => array('label' => __('Conversations', 'ai-eshop-optimizer'), 'icon' => 'messages-square'), 'happiness' => array('label' => __('Happiness', 'ai-eshop-optimizer'), 'icon' => 'star'), 'origins' => array('label' => __('Page origins', 'ai-eshop-optimizer'), 'icon' => 'door-open'), 'products' => array('label' => __('Products and gaps', 'ai-eshop-optimizer'), 'icon' => 'package'), 'conversions' => array('label' => __('Conversions', 'ai-eshop-optimizer'), 'icon' => 'target'), ); } public function render_subtab() { $h = $this->chat_table(); if (!$this->table_exists($h)) { $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-conversations', 'icon' => 'messages-square', 'title' => __('Conversations', 'ai-eshop-optimizer'), 'description' => __('Every chat a shopper had with the AI assistant, with filters to find one and open its full transcript.', 'ai-eshop-optimizer'), )); $this->state_of('not-set-up', array( 'feature' => $this->get_label(), 'text' => __('No chat history yet — the table installs when the AI chat is set up. Once shoppers start chatting, their conversations appear here.', 'ai-eshop-optimizer'), )); echo $close; return; } $base = add_query_arg(array('page' => AIEO_DMM::MENU_SLUG, 'dmm_tab' => $this->get_id()), admin_url('admin.php')); $sections = $this->get_tabs(); $section = isset($_GET['cac_section']) ? sanitize_key(wp_unslash($_GET['cac_section'])) : 'overview'; if (!isset($sections[$section])) { $section = 'overview'; } AIEO_DMM_UI::tab_strip($this->get_id(), $sections, $section, array( 'arg' => $this->get_tab_arg(), 'base' => $base, )); $logging = get_option('aieo_chat_history_enabled', '1'); if ($logging !== '1' && $logging !== 1) { if (class_exists('\Rooster\Admin\States')) { echo \Rooster\Admin\States::off_notice( __('Chat history logging', 'ai-eshop-optimizer'), __('Chat history logging is currently OFF, so new conversations are not being recorded. Existing data is shown below.', 'ai-eshop-optimizer') ); } else { echo '

' . esc_html__('Chat history logging is currently OFF, so new conversations are not being recorded. Existing data is shown below.', 'ai-eshop-optimizer') . '

'; } } $this->band_open(array('cols' => 'overview' === $section ? 2 : 1)); switch ($section) { case 'conversations': $this->render_conversations($base, $h); break; case 'happiness': $this->render_happiness($base, $h); break; case 'origins': $this->render_origins($base, $h); break; case 'products': $this->render_products($base, $h); break; case 'conversions': $this->render_conversions($base, $h); break; case 'overview': default: $this->render_overview($base, $h); break; } $this->band_close(); } private function render_overview($base, $h) { $cache = class_exists('AIEO_Chat_Analytics') ? AIEO_Chat_Analytics::get_cached_stats() : null; $ov = ($cache && isset($cache['overview'])) ? (array) $cache['overview'] : null; $gen = ($cache && !empty($cache['generated_at'])) ? (string) $cache['generated_at'] : ''; if (!$ov) { $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-overview-3', 'icon' => 'hash', 'title' => __('Key chat figures', 'ai-eshop-optimizer'), 'col' => 1, )); $this->render_cache_controls($gen, __('Preparing analytics… this runs in the background and can take a few seconds the first time. The panel refreshes automatically when it’s ready.', 'ai-eshop-optimizer')); echo $close; $this->render_retention_card(); return; } $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-overview-3', 'icon' => 'hash', 'title' => __('Key chat figures', 'ai-eshop-optimizer'), 'description' => __('Reads the cached stats build: messages, conversations and sessions, average happiness out of 100, answers that returned no products, and tokens in and out per message.', 'ai-eshop-optimizer'), 'col' => 1, )); $this->render_cache_controls($gen); if ((int) $ov['turns'] <= 0) { $this->no_chats(); echo $close; $this->render_trend_card(array(), 0); $this->render_retention_card(); return; } echo '
'; $this->tile(__('Messages', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['turns'])); $this->tile(__('Conversations', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['conversations'])); $this->tile(__('Unique sessions', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['sessions'])); $this->tile(__('Logged-in / guest', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['logged_in']) . ' / ' . number_format_i18n((int) $ov['guest'])); $avg = ($ov['avg_rating'] !== null) ? number_format_i18n((float) $ov['avg_rating'], 1) . ' / 100' : '—'; $this->tile(__('Avg happiness', 'ai-eshop-optimizer'), $avg); $rated_pct = ((int) $ov['turns'] > 0) ? round(100 * (int) $ov['rated_turns'] / (int) $ov['turns'], 1) : 0; $this->tile(__('Rated answers', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['rated_turns']) . ' (' . $rated_pct . '%)'); $zero_pct = ((int) $ov['turns'] > 0) ? round(100 * (int) $ov['zero_product_turns'] / (int) $ov['turns'], 1) : 0; $this->tile(__('Answers with 0 products', 'ai-eshop-optimizer'), number_format_i18n((int) $ov['zero_product_turns']) . ' (' . $zero_pct . '%)'); $avg_in = (isset($ov['avg_tokens_in']) && $ov['avg_tokens_in'] !== null && (int) $ov['avg_tokens_in'] > 0) ? number_format_i18n((int) $ov['avg_tokens_in']) : '—'; $this->tile(__('Avg tokens in', 'ai-eshop-optimizer'), $avg_in); $avg_out = (isset($ov['avg_tokens_out']) && $ov['avg_tokens_out'] !== null && (int) $ov['avg_tokens_out'] > 0) ? number_format_i18n((int) $ov['avg_tokens_out']) : '—'; $this->tile(__('Avg tokens out', 'ai-eshop-optimizer'), $avg_out); $max_out = (isset($ov['max_tokens_out']) && $ov['max_tokens_out'] !== null && (int) $ov['max_tokens_out'] > 0) ? number_format_i18n((int) $ov['max_tokens_out']) : '—'; $this->tile(__('Max tokens out', 'ai-eshop-optimizer'), $max_out); $trunc = isset($ov['truncated_turns']) ? (int) $ov['truncated_turns'] : 0; $trunc_pct = ((int) $ov['turns'] > 0) ? round(100 * $trunc / (int) $ov['turns'], 1) : 0; $this->tile(__('Truncated answers', 'ai-eshop-optimizer'), $trunc > 0 ? number_format_i18n($trunc) . ' (' . $trunc_pct . '%)' : '—'); $range = '—'; if (!empty($ov['first_at']) && !empty($ov['last_at'])) { $range = date_i18n('Y-m-d', strtotime($ov['first_at'] . ' UTC')) . ' → ' . date_i18n('Y-m-d', strtotime($ov['last_at'] . ' UTC')); } $this->tile(__('Date range', 'ai-eshop-optimizer'), $range); echo '
'; echo $close; $this->render_trend_card(isset($ov['trend']) ? (array) $ov['trend'] : array(), (int) $ov['turns']); $this->render_retention_card(); } private function render_trend_card($trend, $turns) { $max = 1; foreach ($trend as $t) { $max = max($max, (int) $t['n']); } $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-messages-per-day-last-30-days', 'icon' => 'chart-column', 'title' => __('Chat messages per day', 'ai-eshop-optimizer'), 'description' => __('Shoppers\' chat activity over the last 30 days, to see the busy days and the trend.', 'ai-eshop-optimizer'), 'col' => 2, )); if ($turns <= 0) { $this->no_chats(true); echo $close; return; } $values = array(); $labels = array(); foreach (array_values($trend) as $i => $t) { $values[] = (int) $t['n']; $labels[] = 0 === $i % 7 ? date_i18n('j M', strtotime((string) $t['d'] . ' 00:00:00')) : ''; } if (class_exists('\Rooster\Admin\Ui')) { echo \Rooster\Admin\Ui::chart_svg(array( 'type' => 'bar', 'series' => array(array('label' => __('Chat messages per day', 'ai-eshop-optimizer'), 'values' => $values, 'tone' => 'primary')), 'labels' => $labels, 'label' => __('Chat messages per day', 'ai-eshop-optimizer'), 'height' => 160, 'max' => $max, )); } $this->guidance(__('GMT day buckets.', 'ai-eshop-optimizer')); echo $close; } private function render_retention_card() { $days = (int) get_option('aieo_chat_retention_days', 0); $eff = $days > 0 ? $days : 30; $form_id = 'aieo-card-ai-chat-analytics-history-retention-form'; $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-history-retention', 'icon' => 'timer', 'title' => __('History retention', 'ai-eshop-optimizer'), 'description' => __('How many days of chat history to keep. Older conversations are pruned daily — longer retention means longer trends but more storage. Default is 30 days.', 'ai-eshop-optimizer'), 'essential' => true, 'col' => 2, 'footer' => class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($form_id) : '', )); $field = '

' . '' . esc_html__('days', 'ai-eshop-optimizer') . '

'; echo AIEO_DMM_UI::card_form('aieo_chat_analytics_save_retention', 'ai_chat_analytics_retention', $form_id, $field, array( 'nonce' => wp_create_nonce(self::NONCE), 'nonce_name' => 'nonce', )); echo $close; } private function render_cache_controls($gen, $preparing = '') { $asof = ''; if ($gen !== '') { $asof = sprintf(__('updated %s ago', 'ai-eshop-optimizer'), human_time_diff(strtotime($gen . ' UTC'), current_time('timestamp', true))); } $cadence = self::get_stats_cadence(); $cad_opts = array( 'off' => __('Off', 'ai-eshop-optimizer'), 'hourly' => __('Hourly', 'ai-eshop-optimizer'), 'daily' => __('Daily', 'ai-eshop-optimizer'), 'weekly' => __('Weekly', 'ai-eshop-optimizer'), 'monthly' => __('Monthly', 'ai-eshop-optimizer'), ); if (!class_exists('\Rooster\Admin\States')) { $sel = ''; echo '

' . AIEO_Kit::button(__('Refresh now', 'ai-eshop-optimizer'), array('size' => 'sm', 'attrs' => array('id' => 'aieo-cac-refresh'))) . ' ' . ' ' . esc_html('' !== $preparing ? $preparing : $asof) . '

'; return; } $job = array( 'id' => 'aieo-cac-refresh-job', 'state' => '' !== $preparing ? 'running' : ('' !== $gen ? 'done' : 'not-run'), 'run' => __('Refresh now', 'ai-eshop-optimizer'), 'run_attrs' => array('id' => 'aieo-cac-refresh'), 'primary' => false, 'facts' => '' !== $asof ? array($asof) : array(), ); if ('' !== $preparing) { $job['progress_label'] = $preparing; } echo \Rooster\Admin\States::job($job); echo '
' . \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-cac-cadence', 'label' => __('Auto-refresh', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::select('', $cad_opts, $cadence, array( 'id' => 'aieo-cac-cadence', 'short' => true, 'attrs' => \Rooster\Admin\States::save_attrs(admin_url('admin-ajax.php'), array( 'data' => array( 'action' => 'aieo_chat_analytics_stats_cadence', 'nonce' => wp_create_nonce(self::NONCE), ), 'key' => 'cadence', )), )), )) . '
'; } private function render_conversations($base, $h) { $cid = isset($_GET['cac_cid']) ? sanitize_text_field(wp_unslash($_GET['cac_cid'])) : ''; if ($cid !== '') { $this->render_conversation_thread($base, $h, $cid); return; } global $wpdb; $from = isset($_GET['cac_from']) ? sanitize_text_field(wp_unslash($_GET['cac_from'])) : ''; $to = isset($_GET['cac_to']) ? sanitize_text_field(wp_unslash($_GET['cac_to'])) : ''; $band = isset($_GET['cac_band']) ? sanitize_key(wp_unslash($_GET['cac_band'])) : ''; $aud = isset($_GET['cac_aud']) ? sanitize_key(wp_unslash($_GET['cac_aud'])) : ''; $model = isset($_GET['cac_model']) ? sanitize_text_field(wp_unslash($_GET['cac_model'])) : ''; $q = isset($_GET['cac_q']) ? sanitize_text_field(wp_unslash($_GET['cac_q'])) : ''; $sort = isset($_GET['cac_sort']) ? sanitize_key(wp_unslash($_GET['cac_sort'])) : 'new'; $where = array('1=1'); $params = array(); if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) { $where[] = 'created_at >= %s'; $params[] = get_gmt_from_date($from . ' 00:00:00'); } if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) { $where[] = 'created_at <= %s'; $params[] = get_gmt_from_date($to . ' 23:59:59'); } $bw = $this->rating_band_where($band); if ($bw !== '') { $where[] = $bw; } if ($aud === 'in') { $where[] = 'user_id > 0'; } elseif ($aud === 'guest') { $where[] = '(user_id IS NULL OR user_id = 0)'; } if ($model !== '') { $where[] = 'model_used = %s'; $params[] = $model; } if ($q !== '') { $where[] = 'user_message LIKE %s'; $params[] = '%' . $wpdb->esc_like($q) . '%'; } $where_sql = implode(' AND ', $where); $sort_map = array( 'new' => 'created_at DESC', 'old' => 'created_at ASC', 'low' => 'user_rating ASC, created_at DESC', 'high' => 'user_rating DESC, created_at DESC', ); $order_by = isset($sort_map[$sort]) ? $sort_map[$sort] : $sort_map['new']; $count_sql = "SELECT COUNT(*) FROM {$h} WHERE {$where_sql}"; $total = (int) ($params ? $wpdb->get_var($wpdb->prepare($count_sql, $params)) : $wpdb->get_var($count_sql)); $per = 50; $pageno = isset($_GET['cac_p']) ? max(1, (int) $_GET['cac_p']) : 1; $offset = ($pageno - 1) * $per; $sql = "SELECT id, conversation_id, session_id, user_id, user_message, assistant_message, model_used, products_found, user_rating, created_at FROM {$h} WHERE {$where_sql} ORDER BY {$order_by} LIMIT %d OFFSET %d"; $rows = $wpdb->get_results($wpdb->prepare($sql, array_merge($params, array($per, $offset))), ARRAY_A); $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-conversations', 'icon' => 'messages-square', 'title' => __('Conversations', 'ai-eshop-optimizer'), 'description' => __('Every chat a shopper had with the AI assistant, with filters to find one and open its full transcript.', 'ai-eshop-optimizer'), 'flush' => true, )); $filtered = count($where) > 1; if (!$filtered && 0 === $total) { $this->no_chats(); echo $close; return; } $this->render_conversations_filter($base, compact('from', 'to', 'band', 'aud', 'model', 'q', 'sort'), $h); $facts = array(); foreach (array( array(__('From', 'ai-eshop-optimizer'), $from), array(__('To', 'ai-eshop-optimizer'), $to), array(__('Happiness', 'ai-eshop-optimizer'), '' !== $band ? (string) ($this->band_labels()[$band] ?? $band) : ''), array(__('Audience', 'ai-eshop-optimizer'), '' !== $aud ? (string) ($this->audience_labels()[$aud] ?? $aud) : ''), array(__('Model', 'ai-eshop-optimizer'), $model), array(__('Search question', 'ai-eshop-optimizer'), '' !== $q ? '“' . $q . '”' : ''), ) as $fact) { if ('' !== $fact[1]) { $facts[] = $fact[0] . ': ' . $fact[1]; } } $count = rtrim(sprintf(__('%s matching messages.', 'ai-eshop-optimizer'), number_format_i18n($total)), '.'); if (class_exists('\Rooster\Admin\States')) { \Rooster\Admin\States::enqueue(); echo \Rooster\Admin\States::result_head($count, array( 'facts' => $facts, 'actions' => $filtered ? \Rooster\Admin\Ui::button(__('Clear filters', 'ai-eshop-optimizer'), array( 'href' => add_query_arg(array('cac_section' => 'conversations'), $base), 'icon' => 'x', 'size' => 'sm', 'variant' => 'ghost', )) : '', )); } else { echo '

' . esc_html($count) . '

'; } $aieo_cac_rows = array(); foreach ($rows as $r) { $pf = json_decode((string) $r['products_found'], true); $aieo_cac_rows[] = array( 'id' => (int) $r['id'], 'when' => (string) $r['created_at'], 'user_id' => (int) $r['user_id'], 'question' => (string) $r['user_message'], 'answer' => (string) $r['assistant_message'], 'products' => is_array($pf) ? count($pf) : 0, 'rating' => $r['user_rating'], 'turl' => add_query_arg(array('cac_section' => 'conversations', 'cac_cid' => $r['conversation_id']), $base), ); } AIEO_Table::render(array( 'id' => 'aieo-cac-conversations', 'page_id' => 'ai_chat_analytics', 'row_key' => 'id', 'sortable' => false, 'per_page' => 0, 'rows' => $aieo_cac_rows, 'empty' => __('No messages match these filters.', 'ai-eshop-optimizer'), 'columns' => array( array('key' => 'when', 'label' => __('When', 'ai-eshop-optimizer'), 'type' => 'datetime', 'format' => array('tz' => 'utc'), 'nowrap' => true, 'width' => '130px'), array( 'key' => 'user_id', 'label' => __('Who', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '120px', 'format' => array('render' => function ($r) { return ((int) $r['user_id'] > 0) ? $this->user_label((int) $r['user_id']) : esc_html__('Guest', 'ai-eshop-optimizer'); }), ), array( 'key' => 'question', 'label' => __('Question', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => function ($r) { return $this->expander($r['question']); }), ), array( 'key' => 'answer', 'label' => __('Answer', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => function ($r) { return $this->expander($r['answer']); }), ), array('key' => 'products', 'label' => __('Products', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '60px'), array( 'key' => 'rating', 'label' => __('Rating', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '80px', 'bands' => array( array('min' => 61, 'tone' => 'success'), array('min' => 40, 'max' => 60, 'tone' => 'warning'), array('max' => 39, 'tone' => 'danger'), ), ), array( 'key' => 'turl', 'label' => __('Thread', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '80px', 'format' => array('render' => static function ($r) { return AIEO_Kit::button(__('View', 'ai-eshop-optimizer'), array('href' => $r['turl'], 'size' => 'sm')); }), ), ), )); $this->render_pager($base, $total, $per, $pageno, array( 'cac_section' => 'conversations', 'cac_from' => $from, 'cac_to' => $to, 'cac_band' => $band, 'cac_aud' => $aud, 'cac_model' => $model, 'cac_q' => $q, 'cac_sort' => $sort, )); echo $close; } private function render_conversations_filter($base, $f, $h) { global $wpdb; $models = $wpdb->get_col("SELECT DISTINCT model_used FROM {$h} WHERE model_used <> '' ORDER BY model_used ASC"); $filter = static function ($label, $control, $id) { if (class_exists('\Rooster\Admin\Ui')) { return \Rooster\Admin\Ui::filter((string) $label, $control, $id); } return ''; }; $select = static function ($id, $name, array $options, $current) { return AIEO_Kit::select($name, $options, (string) $current, array('id' => $id)); }; echo '
'; echo ''; echo ''; echo ''; echo $filter(__('From', 'ai-eshop-optimizer'), AIEO_Kit::input('cac_from', (string) $f['from'], array('type' => 'date', 'id' => 'aieo-cac-f-from')), 'aieo-cac-f-from'); echo $filter(__('To', 'ai-eshop-optimizer'), AIEO_Kit::input('cac_to', (string) $f['to'], array('type' => 'date', 'id' => 'aieo-cac-f-to')), 'aieo-cac-f-to'); $bands = array('' => __('Any', 'ai-eshop-optimizer')) + $this->band_labels(); echo $filter(__('Happiness', 'ai-eshop-optimizer'), $select('aieo-cac-f-band', 'cac_band', $bands, $f['band']), 'aieo-cac-f-band'); $auds = array('' => __('All', 'ai-eshop-optimizer')) + $this->audience_labels(); echo $filter(__('Audience', 'ai-eshop-optimizer'), $select('aieo-cac-f-aud', 'cac_aud', $auds, $f['aud']), 'aieo-cac-f-aud'); $model_opts = array('' => __('Any', 'ai-eshop-optimizer')); foreach ((array) $models as $m) { $model_opts[(string) $m] = (string) $m; } echo $filter(__('Model', 'ai-eshop-optimizer'), $select('aieo-cac-f-model', 'cac_model', $model_opts, $f['model']), 'aieo-cac-f-model'); $sorts = array('new' => __('Newest', 'ai-eshop-optimizer'), 'old' => __('Oldest', 'ai-eshop-optimizer'), 'low' => __('Lowest rating', 'ai-eshop-optimizer'), 'high' => __('Highest rating', 'ai-eshop-optimizer')); echo $filter(__('Sort', 'ai-eshop-optimizer'), $select('aieo-cac-f-sort', 'cac_sort', $sorts, $f['sort']), 'aieo-cac-f-sort'); echo $filter(__('Search question', 'ai-eshop-optimizer'), AIEO_Kit::input('cac_q', (string) $f['q'], array('type' => 'search', 'id' => 'aieo-cac-f-q', 'medium' => true)), 'aieo-cac-f-q'); echo '
' . AIEO_Kit::button(__('Filter', 'ai-eshop-optimizer'), array('type' => 'submit')) . '
'; echo '
'; } private function band_labels() { return array( 'happy' => __('Happy (>60)', 'ai-eshop-optimizer'), 'neutral' => __('Neutral (40–60)', 'ai-eshop-optimizer'), 'unhappy' => __('Unhappy (<40)', 'ai-eshop-optimizer'), 'unrated' => __('Unrated', 'ai-eshop-optimizer'), ); } private function audience_labels() { return array( 'in' => __('Logged in', 'ai-eshop-optimizer'), 'guest' => __('Guests', 'ai-eshop-optimizer'), ); } private function render_conversation_thread($base, $h, $cid) { global $wpdb; $rows = $wpdb->get_results($wpdb->prepare( "SELECT id, session_id, user_id, user_message, assistant_message, model_used, products_found, potential_suggestions_found, user_rating, created_at FROM {$h} WHERE conversation_id = %s ORDER BY created_at ASC, id ASC", $cid ), ARRAY_A); $back_link = AIEO_Kit::button(__('Back to conversations', 'ai-eshop-optimizer'), array('href' => add_query_arg(array('cac_section' => 'conversations'), $base), 'size' => 'sm')); $back = '

' . $back_link . '

'; if (empty($rows)) { $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-conversation-2', 'icon' => 'message-square-text', 'title' => __('Conversation', 'ai-eshop-optimizer'), )); $this->empty_list(__('This conversation no longer exists (it may have been pruned).', 'ai-eshop-optimizer'), $back_link); echo $close; return; } $first = $rows[0]; $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-conversation-2', 'icon' => 'message-square-text', 'title' => __('Conversation', 'ai-eshop-optimizer'), 'tools' => '' . esc_html($cid) . '', )); $who = ((int) $first['user_id'] > 0) ? $this->user_label((int) $first['user_id']) : esc_html__('Guest', 'ai-eshop-optimizer'); echo '

' . esc_html__('Who:', 'ai-eshop-optimizer') . ' ' . $who . ' · ' . esc_html__('Turns:', 'ai-eshop-optimizer') . ' ' . count($rows) . ' · ' . esc_html__('Model:', 'ai-eshop-optimizer') . ' ' . esc_html((string) $first['model_used']) . ' · ' . esc_html__('Session:', 'ai-eshop-optimizer') . ' ' . esc_html((string) $first['session_id']) . '

' . $back; $sections = function_exists('rooster_admin_card_section_html') && class_exists('AIEO_Chrome') && AIEO_Chrome::on(); foreach ($rows as $r) { $when = date_i18n('Y-m-d H:i', strtotime($r['created_at'] . ' UTC')); echo $sections ? rooster_admin_card_section_html() . '
' : '
'; echo '

' . esc_html($when) . ' · ' . $this->rating_chip($r['user_rating']) . '

'; echo '

' . esc_html__('Q:', 'ai-eshop-optimizer') . ' ' . nl2br(esc_html((string) $r['user_message'])) . '

'; echo '

' . esc_html__('A:', 'ai-eshop-optimizer') . ' ' . nl2br(esc_html((string) $r['assistant_message'])) . '

'; $pf = json_decode((string) $r['products_found'], true); if (is_array($pf) && $pf) { $names = array(); foreach (array_slice($pf, 0, 12) as $pid) { $t = get_the_title((int) $pid); $names[] = ($t !== '') ? $t : ('#' . (int) $pid); } $aieo_ca_title = sprintf(__('%d products surfaced', 'ai-eshop-optimizer'), count($pf)); $aieo_ca_body = '

' . esc_html(implode(', ', $names)) . '

'; echo class_exists('\Rooster\Admin\Ui') ? \Rooster\Admin\Ui::fold($aieo_ca_title, $aieo_ca_body) : '
' . esc_html($aieo_ca_title) . '' . $aieo_ca_body . '
'; } echo '
'; } echo $close; } private function render_happiness($base, $h) { global $wpdb; $d = $wpdb->get_row( "SELECT SUM(CASE WHEN user_rating > 60 THEN 1 ELSE 0 END) AS happy, SUM(CASE WHEN user_rating BETWEEN 40 AND 60 THEN 1 ELSE 0 END) AS neutral, SUM(CASE WHEN user_rating < 40 AND user_rating IS NOT NULL THEN 1 ELSE 0 END) AS unhappy, SUM(CASE WHEN user_rating IS NULL THEN 1 ELSE 0 END) AS unrated, AVG(user_rating) AS avg_r, SUM(CASE WHEN user_rating IS NOT NULL THEN 1 ELSE 0 END) AS rated, COUNT(*) AS total FROM {$h}", ARRAY_A ); $d = is_array($d) ? $d : array(); $happy = (int) ($d['happy'] ?? 0); $neutral = (int) ($d['neutral'] ?? 0); $unhappy = (int) ($d['unhappy'] ?? 0); $unrated = (int) ($d['unrated'] ?? 0); $rated = (int) ($d['rated'] ?? 0); $total = (int) ($d['total'] ?? 0); $avg = ($rated > 0 && isset($d['avg_r']) && $d['avg_r'] !== null) ? round((float) $d['avg_r'], 1) : null; $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-happiness', 'icon' => 'star', 'title' => __('Happiness', 'ai-eshop-optimizer'), 'description' => __('How shoppers rated the AI assistant\'s answers, and how those ratings are spread.', 'ai-eshop-optimizer'), )); if ($total <= 0) { $this->no_chats(); echo $close; } else { $this->render_happiness_figures($happy, $neutral, $unhappy, $unrated, $rated, $total, $avg); echo $close; } $this->render_lowest_rated($base, $h); } private function render_happiness_figures($happy, $neutral, $unhappy, $unrated, $rated, $total, $avg) { echo '
'; $this->tile(__('Avg rating', 'ai-eshop-optimizer'), $avg !== null ? number_format_i18n($avg, 1) . ' / 100' : '—'); $this->tile(__('Rated answers', 'ai-eshop-optimizer'), number_format_i18n($rated) . ($total > 0 ? ' (' . round(100 * $rated / $total, 1) . '%)' : '')); $this->tile(__('Happy', 'ai-eshop-optimizer'), number_format_i18n($happy)); $this->tile(__('Neutral', 'ai-eshop-optimizer'), number_format_i18n($neutral)); $this->tile(__('Unhappy', 'ai-eshop-optimizer'), number_format_i18n($unhappy)); echo '
'; $rated_total = $happy + $neutral + $unhappy; $aieo_cac_bands = array(); foreach (array( array($happy, 'success', __('Happy (>60)', 'ai-eshop-optimizer')), array($neutral, 'warning', __('Neutral (40–60)', 'ai-eshop-optimizer')), array($unhappy, 'danger', __('Unhappy (<40)', 'ai-eshop-optimizer')), ) as $bd) { $aieo_cac_bands[] = array( 'band' => $bd[2], 'pct' => $rated_total > 0 ? round(100 * $bd[0] / $rated_total, 1) : 0, 'tone' => $bd[1], 'count' => $bd[0], ); } AIEO_Table::render(array( 'id' => 'aieo-cac-happiness-bands', 'page_id' => 'ai_chat_analytics', 'sortable' => false, 'rows' => $aieo_cac_bands, 'columns' => array( array('key' => 'band', 'label' => __('Band', 'ai-eshop-optimizer'), 'type' => 'text', 'width' => '160px'), array( 'key' => 'pct', 'label' => __('Distribution', 'ai-eshop-optimizer'), 'type' => 'bar', 'tone_key' => 'tone', 'format' => array('max' => 100, 'precision' => 1), 'suffix' => '%', ), array('key' => 'count', 'label' => __('Count', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '90px'), ), )); echo '

' . esc_html(sprintf(__('%s answers were left unrated.', 'ai-eshop-optimizer'), number_format_i18n($unrated))) . '

'; } private function render_lowest_rated($base, $h) { global $wpdb; $low = $wpdb->get_results("SELECT conversation_id, user_message, user_rating, created_at FROM {$h} WHERE user_rating IS NOT NULL ORDER BY user_rating ASC, created_at DESC LIMIT 25", ARRAY_A); $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-lowest-rated-answers', 'icon' => 'x', 'title' => __('Lowest-rated answers', 'ai-eshop-optimizer'), 'description' => __('Where shoppers were least happy — review these to improve answers or catalogue coverage.', 'ai-eshop-optimizer'), 'flush' => true, )); $aieo_cac_low = array(); foreach ($low as $r) { $aieo_cac_low[] = array( 'rating' => $r['user_rating'], 'question' => (string) $r['user_message'], 'when' => (string) $r['created_at'], 'turl' => add_query_arg(array('cac_section' => 'conversations', 'cac_cid' => $r['conversation_id']), $base), ); } AIEO_Table::render(array( 'id' => 'aieo-cac-lowest-rated', 'page_id' => 'ai_chat_analytics', 'sortable' => false, 'rows' => $aieo_cac_low, 'per_page' => 0, 'cap' => 25, 'capped' => count($low) >= 25, 'empty' => __('No ratings yet.', 'ai-eshop-optimizer'), 'columns' => array( array( 'key' => 'rating', 'label' => __('Rating', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '80px', 'bands' => array( array('min' => 61, 'tone' => 'success'), array('min' => 40, 'max' => 60, 'tone' => 'warning'), array('max' => 39, 'tone' => 'danger'), ), ), array( 'key' => 'question', 'label' => __('Question', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => function ($r) { return $this->expander($r['question']); }), ), array('key' => 'when', 'label' => __('When', 'ai-eshop-optimizer'), 'type' => 'datetime', 'format' => array('tz' => 'utc'), 'nowrap' => true, 'width' => '130px'), array( 'key' => 'turl', 'label' => __('Thread', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '80px', 'format' => array('render' => static function ($r) { return AIEO_Kit::button(__('View', 'ai-eshop-optimizer'), array('href' => $r['turl'], 'size' => 'sm')); }), ), ), )); echo $close; } private function render_origins($base, $h) { global $wpdb; $se = $wpdb->prefix . 'aieo_sessions'; $ss = $wpdb->prefix . 'aieo_sessions_statics'; $se_exists = $this->table_exists($se); $ss_exists = $this->table_exists($ss); if (!$se_exists && !$ss_exists) { $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-where-shoppers-start-chats', 'icon' => 'door-open', 'title' => __('Where shoppers start chats', 'ai-eshop-optimizer'), )); $this->state_of('not-set-up', array( 'text' => __('Page origins come from the Session Tracker, which isn’t active yet. Enable Session Tracking (Customer Profiling → User Vitals) to see where shoppers start chats.', 'ai-eshop-optimizer'), 'href' => $this->session_tracker_url(), 'label' => __('AI Session Tracker', 'ai-eshop-optimizer'), )); echo $close; return; } $rows = array(); if ($se_exists) { $rows = $wpdb->get_results("SELECT page_url, COUNT(*) AS n FROM {$se} WHERE event_type IN ('chat_open','chat_message') AND page_url <> '' GROUP BY page_url ORDER BY n DESC LIMIT 500", ARRAY_A); } $buckets = array(); $urls = array(); $total = 0; foreach ((array) $rows as $r) { $n = (int) $r['n']; $total += $n; $b = $this->classify_origin($r['page_url']); $buckets[$b] = isset($buckets[$b]) ? $buckets[$b] + $n : $n; $urls[] = array('url' => $r['page_url'], 'n' => $n); } $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-where-shoppers-start-chats', 'icon' => 'door-open', 'title' => __('Where shoppers start chats', 'ai-eshop-optimizer'), 'description' => __('Recorded by the Session Tracker when a shopper opens the chat or sends a message.', 'ai-eshop-optimizer'), 'flush' => $total > 0, )); if ($total === 0) { $this->state_of('first-use', array('text' => __('No chat-origin events recorded yet — they start accruing once shoppers chat with Session Tracking on. The fallback below uses each chat session’s landing page in the meantime.', 'ai-eshop-optimizer'))); } else { arsort($buckets); $aieo_cac_orows = array(); foreach ($buckets as $b => $n) { $aieo_cac_orows[] = array( 'type' => $this->origin_label($b), 'pct' => $total > 0 ? round(100 * $n / $total, 1) : 0, 'chats' => $n, ); } AIEO_Table::render(array( 'id' => 'aieo-cac-origin-types', 'page_id' => 'ai_chat_analytics', 'sortable' => false, 'rows' => $aieo_cac_orows, 'columns' => array( array('key' => 'type', 'label' => __('Page type', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'pct', 'label' => __('Share', 'ai-eshop-optimizer'), 'type' => 'bar', 'tone' => 'info', 'format' => array('max' => 100, 'precision' => 1), 'suffix' => '%'), array('key' => 'chats', 'label' => __('Chats', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '90px'), ), )); if (function_exists('rooster_admin_card_section_html') && class_exists('AIEO_Chrome') && AIEO_Chrome::on()) { echo rooster_admin_card_section_html(__('Top pages', 'ai-eshop-optimizer')); } else { echo '

' . esc_html__('Top pages', 'ai-eshop-optimizer') . '

'; } $aieo_cac_urls = array(); foreach (array_slice($urls, 0, 20) as $u) { $aieo_cac_urls[] = array('url' => (string) $u['url'], 'chats' => (int) $u['n']); } AIEO_Table::render(array( 'id' => 'aieo-cac-origin-pages', 'page_id' => 'ai_chat_analytics', 'rows' => $aieo_cac_urls, 'cap' => 20, 'capped' => count($urls) > 20, 'total' => count($urls), 'columns' => array( array('key' => 'url', 'label' => __('URL', 'ai-eshop-optimizer'), 'type' => 'code'), array('key' => 'chats', 'label' => __('Chats', 'ai-eshop-optimizer'), 'type' => 'integer'), ), )); } echo $close; if ($ss_exists) { $this->render_origin_fallback($h, $ss); } } private function render_origin_fallback($h, $ss) { global $wpdb; $cast = $this->collation_cast($h, $ss, 'session_id'); $wpdb->suppress_errors(true); $rows = $wpdb->get_results( "SELECT ss.landing_page AS url, COUNT(DISTINCT ss.session_id) AS n FROM (SELECT DISTINCT session_id FROM {$h} WHERE session_id <> '') ch JOIN {$ss} ss ON ss.session_id = ch.session_id{$cast} WHERE ss.landing_page <> '' GROUP BY ss.landing_page ORDER BY n DESC LIMIT 300", ARRAY_A ); $wpdb->suppress_errors(false); $buckets = array(); $total = 0; foreach ((array) $rows as $r) { $n = (int) $r['n']; $total += $n; $b = $this->classify_origin($r['url']); $buckets[$b] = isset($buckets[$b]) ? $buckets[$b] + $n : $n; } $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-chat-session-landing-pages', 'icon' => 'file-pen-line', 'title' => __('Chat session landing pages', 'ai-eshop-optimizer'), 'description' => __('Where chat sessions first landed on the site — covers chats from before per-chat origin tracking, approximated by the session’s entry page.', 'ai-eshop-optimizer'), 'flush' => $total > 0, )); if ($total === 0) { $this->state_of('first-use', array('title' => __('No linked session data yet.', 'ai-eshop-optimizer'))); } else { arsort($buckets); $aieo_cac_frows = array(); foreach ($buckets as $b => $n) { $aieo_cac_frows[] = array( 'type' => $this->origin_label($b), 'pct' => $total > 0 ? round(100 * $n / $total, 1) : 0, 'sessions' => $n, ); } AIEO_Table::render(array( 'id' => 'aieo-cac-landing-types', 'page_id' => 'ai_chat_analytics', 'sortable' => false, 'rows' => $aieo_cac_frows, 'columns' => array( array('key' => 'type', 'label' => __('Page type', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'pct', 'label' => __('Share', 'ai-eshop-optimizer'), 'type' => 'bar', 'tone' => 'success', 'format' => array('max' => 100, 'precision' => 1), 'suffix' => '%'), array('key' => 'sessions', 'label' => __('Sessions', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '90px'), ), )); } echo $close; } private function render_products($base, $h) { global $wpdb; $cache = class_exists('AIEO_Chat_Analytics') ? AIEO_Chat_Analytics::get_cached_stats() : null; $products = ($cache && isset($cache['products'])) ? (array) $cache['products'] : null; $gen = ($cache && !empty($cache['generated_at'])) ? (string) $cache['generated_at'] : ''; $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-most-recommended-products', 'icon' => 'package', 'title' => __('Most-recommended products', 'ai-eshop-optimizer'), 'description' => __('Products the assistant surfaced most often across all chats (precomputed).', 'ai-eshop-optimizer'), 'flush' => !empty($products), )); $this->render_cache_controls($gen, null === $products ? __('Preparing… building in the background.', 'ai-eshop-optimizer') : ''); if (is_array($products) && empty($products)) { $this->state_of('first-use', array('title' => __('No products surfaced in chats yet.', 'ai-eshop-optimizer'))); } elseif (!empty($products)) { $aieo_cac_prows = array(); foreach ($products as $i => $p) { $aieo_cac_prows[] = array( 'rank' => (int) ($i + 1), 'product' => (string) $p['title'], 'pid' => (int) $p['post_id'], 'edit_url' => (string) get_edit_post_link((int) $p['post_id']), 'recommended' => (int) $p['recommended'], ); } AIEO_Table::render(array( 'id' => 'aieo-cac-top-products', 'page_id' => 'ai_chat_analytics', 'rows' => $aieo_cac_prows, 'columns' => array( array('key' => 'rank', 'label' => '#', 'type' => 'integer', 'width' => '50px', 'sortable' => false), array( 'key' => 'product', 'label' => __('Product', 'ai-eshop-optimizer'), 'type' => 'link', 'format' => array('href' => 'edit_url'), 'suffix' => static function ($r) { return '#' . (int) $r['pid']; }, ), array('key' => 'recommended', 'label' => __('Times recommended', 'ai-eshop-optimizer'), 'type' => 'integer'), ), )); } echo $close; $rows = $wpdb->get_results("SELECT conversation_id, user_message, user_rating, created_at FROM {$h} WHERE (products_found IS NULL OR products_found = '' OR products_found = '[]') ORDER BY created_at DESC LIMIT 50", ARRAY_A); $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-questions-that-returned-no-products', 'icon' => 'file-question-mark', 'title' => __('Questions that returned no products', 'ai-eshop-optimizer'), 'description' => __('Catalogue / answer gaps — questions where the assistant found nothing to recommend. Good candidates for new products, better descriptions, or business-info entries.', 'ai-eshop-optimizer'), 'flush' => true, )); $aieo_cac_gap_rows = array(); foreach ($rows as $r) { $aieo_cac_gap_rows[] = array( 'when' => (string) $r['created_at'], 'question' => (string) $r['user_message'], 'rating' => $r['user_rating'], 'turl' => add_query_arg(array('cac_section' => 'conversations', 'cac_cid' => $r['conversation_id']), $base), ); } AIEO_Table::render(array( 'id' => 'aieo-cac-no-products', 'page_id' => 'ai_chat_analytics', 'sortable' => false, 'rows' => $aieo_cac_gap_rows, 'per_page' => 0, 'cap' => 50, 'capped' => count($rows) >= 50, 'empty' => __('None — every answer surfaced at least one product.', 'ai-eshop-optimizer'), 'columns' => array( array('key' => 'when', 'label' => __('When', 'ai-eshop-optimizer'), 'type' => 'datetime', 'format' => array('tz' => 'utc'), 'nowrap' => true, 'width' => '130px'), array( 'key' => 'question', 'label' => __('Question', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => function ($r) { return $this->expander($r['question']); }), ), array( 'key' => 'rating', 'label' => __('Rating', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '80px', 'bands' => array( array('min' => 61, 'tone' => 'success'), array('min' => 40, 'max' => 60, 'tone' => 'warning'), array('max' => 39, 'tone' => 'danger'), ), ), array( 'key' => 'turl', 'label' => __('Thread', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '80px', 'format' => array('render' => static function ($r) { return AIEO_Kit::button(__('View', 'ai-eshop-optimizer'), array('href' => $r['turl'], 'size' => 'sm')); }), ), ), )); echo $close; } private function render_conversions($base, $h) { unset($base, $h); $cache = class_exists('AIEO_Chat_Analytics') ? AIEO_Chat_Analytics::get_cached_stats() : null; $c = ($cache && isset($cache['conversions'])) ? (array) $cache['conversions'] : null; $gen = ($cache && !empty($cache['generated_at'])) ? (string) $cache['generated_at'] : ''; $close = $this->card_open(array( 'anchor' => 'ai-chat-analytics-chat-assisted-conversions', 'icon' => 'target', 'title' => __('Chat-assisted conversions', 'ai-eshop-optimizer'), 'description' => __('Sessions where a shopper used the AI chat AND placed an order. Session-coincidence attribution (the chat was part of the journey), not last-click — computed in the background from the Session Tracker, never on page load.', 'ai-eshop-optimizer'), )); $building = null === $c; $this->render_cache_controls($gen, $building ? __('Preparing… building in the background.', 'ai-eshop-optimizer') : ''); if (!$building && empty($c['available'])) { $this->state_of('not-set-up', array( 'text' => __('Session Tracking isn’t active yet, so order data can’t be linked to chats. Enable Session Tracking to see chat-assisted conversions.', 'ai-eshop-optimizer'), 'href' => $this->session_tracker_url(), 'label' => __('AI Session Tracker', 'ai-eshop-optimizer'), )); } elseif (!$building && 0 === (int) $c['chat_sessions']) { $this->no_chats(); } elseif (!$building) { $cur = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : ''; echo '
'; $this->tile(__('Chat sessions', 'ai-eshop-optimizer'), number_format_i18n((int) $c['chat_sessions'])); $this->tile(__('Converted', 'ai-eshop-optimizer'), number_format_i18n((int) $c['converted'])); $this->tile(__('Conversion rate', 'ai-eshop-optimizer'), number_format_i18n((float) $c['rate'], 1) . '%'); $this->tile(__('Assisted revenue', 'ai-eshop-optimizer'), $cur . number_format_i18n((float) $c['revenue'], 2)); $this->tile(__('Avg order value', 'ai-eshop-optimizer'), $cur . number_format_i18n((float) $c['aov'], 2)); echo '
'; } echo $close; } private function card_open(array $c) { list($open, $close) = AIEO_DMM_UI::card($c); echo $open; return $close; } private function band_open(array $opts) { if (function_exists('rooster_admin_band_open')) { rooster_admin_band_open($opts); } } private function band_close() { if (function_exists('rooster_admin_band_close')) { rooster_admin_band_close(); } } private function tiles_class() { return class_exists('\Rooster\Admin\Ui') ? 'rooster-admin-stats' : 'aieo-attr-tiles'; } private function tile($label, $value) { if (class_exists('\Rooster\Admin\Ui')) { echo \Rooster\Admin\Ui::stat((string) $label, (string) $value); return; } echo '
' . esc_html($label) . '
' . esc_html((string) $value) . '
'; } private function state_of($kind, array $opts) { if (class_exists('\Rooster\Admin\States')) { echo \Rooster\Admin\States::state($kind, $opts); return; } echo '

' . esc_html(trim((string) ($opts['title'] ?? '') . ' ' . (string) ($opts['text'] ?? ''))) . '

'; } private function no_chats($compact = false) { $this->state_of('first-use', array( 'title' => __('No chats yet', 'ai-eshop-optimizer'), 'text' => $compact ? '' : __('Once shoppers start chatting, their conversations appear here.', 'ai-eshop-optimizer'), 'compact' => $compact, )); } private function guidance($text) { echo class_exists('\Rooster\Admin\Guidance') ? \Rooster\Admin\Guidance::text((string) $text) : '

' . esc_html($text) . '

'; } private function session_tracker_url() { return add_query_arg(array('page' => AIEO_DMM::MENU_SLUG, 'dmm_tab' => 'session_tracker'), admin_url('admin.php')); } private function empty_list($title, $action = '') { if (class_exists('\Rooster\Admin\Ui')) { echo \Rooster\Admin\Ui::empty_state((string) $title, '', array('compact' => true, 'action' => $action)); return; } echo '

' . esc_html($title) . '

' . $action; } private function rating_band_where($band) { switch ($band) { case 'happy': return 'user_rating > 60'; case 'neutral': return 'user_rating BETWEEN 40 AND 60'; case 'unhappy': return 'user_rating < 40 AND user_rating IS NOT NULL'; case 'unrated': return 'user_rating IS NULL'; default: return ''; } } private function rating_chip($rating) { if ($rating === null || $rating === '') { return '' . esc_html__('Unrated', 'ai-eshop-optimizer') . ''; } $r = (int) $rating; if (class_exists('\Rooster\Admin\Ui')) { return \Rooster\Admin\Ui::badge((string) $r, $r > 60 ? 'success' : ($r >= 40 ? 'warning' : 'destructive'), array('sm' => true)); } return (string) (int) $r; } private function expander($text) { $plain = trim(wp_strip_all_tags((string) $text)); if ($plain === '') { return '' . esc_html__('None', 'ai-eshop-optimizer') . ''; } if (mb_strlen($plain) <= 90) { return '' . esc_html($plain) . ''; } $short = wp_trim_words($plain, 16, '…'); return '
' . esc_html($short) . '

' . nl2br(esc_html($plain)) . '

'; } private function user_label($uid) { $u = get_user_by('id', $uid); if (!$u) { return '#' . (int) $uid; } $name = $u->display_name ? $u->display_name : $u->user_login; $edit = get_edit_user_link($uid); return $edit ? '' . esc_html($name) . '' : esc_html($name); } private function render_pager($base, $total, $per, $pageno, $args) { $pages = (int) ceil($total / max(1, $per)); if ($pages <= 1) { return; } $url = static function ($n) use ($base, $args) { return add_query_arg(array_merge($args, array('cac_p' => (int) $n)), $base); }; if (class_exists('\Rooster\Admin\Ui')) { echo \Rooster\Admin\Ui::pager((int) $pageno, $pages, $url, array('per_page' => (int) $per)); return; } echo '

'; for ($i = 1; $i <= min($pages, 12); $i++) { echo $i === (int) $pageno ? ' ' . (int) $i . ' ' : ' ' . (int) $i . ' '; } echo '

'; } private function classify_origin($url) { $path = strtolower((string) wp_parse_url((string) $url, PHP_URL_PATH)); if ($path === '' || $path === '/') { return 'home'; } if (strpos($path, '/product/') !== false) { return 'product'; } if (strpos($path, '/product-category/') !== false || strpos($path, '/brand') !== false || strpos($path, '/need') !== false) { return 'category'; } if (strpos($path, '/checkout') !== false) { return 'checkout'; } if (strpos($path, '/cart') !== false) { return 'cart'; } if (strpos($path, '/shop') !== false) { return 'shop'; } if (strpos($path, '/my-account') !== false) { return 'account'; } if (strpos($path, '/blog') !== false || strpos($path, '/category/') !== false || strpos($path, '/tag/') !== false) { return 'blog'; } return 'other'; } private function origin_label($b) { $m = array( 'home' => __('Home', 'ai-eshop-optimizer'), 'product' => __('Product page', 'ai-eshop-optimizer'), 'category' => __('Category / brand / need', 'ai-eshop-optimizer'), 'checkout' => __('Checkout', 'ai-eshop-optimizer'), 'cart' => __('Cart', 'ai-eshop-optimizer'), 'shop' => __('Shop', 'ai-eshop-optimizer'), 'account' => __('My account', 'ai-eshop-optimizer'), 'blog' => __('Blog', 'ai-eshop-optimizer'), 'other' => __('Other', 'ai-eshop-optimizer'), ); return isset($m[$b]) ? $m[$b] : $b; } private function chat_table() { global $wpdb; return $wpdb->prefix . 'aieo_chat_history'; } private function table_exists($name) { global $wpdb; static $cache = array(); if (!array_key_exists($name, $cache)) { $cache[$name] = ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $name)) === $name); } return $cache[$name]; } private function collation_cast($table_a, $table_b, $col) { $a = $this->column_collation($table_a, $col); $b = $this->column_collation($table_b, $col); if ($a !== '' && $b !== '' && $a !== $b && preg_match('/^[A-Za-z0-9_]+$/', $b)) { return ' COLLATE ' . $b; } return ''; } private function column_collation($table, $col) { global $wpdb; $v = $wpdb->get_var($wpdb->prepare( "SELECT COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s", DB_NAME, $table, $col )); return $v ? (string) $v : ''; } }