__('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') . '
' . '' . 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 '' . 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 ''; } 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']) . '
' . 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(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(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 '' . nl2br(esc_html($plain)) . '
'; 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 : ''; } }