PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.3
MxChat – AI Chatbot & Content Generation for WordPress v3.1.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-admin.php +331 -2517 3.2.123.1.3 View file →
@@ -37,22 +37,11 @@
37 37 }
38 38
39 39 // Add admin menu and initialize settings
40 40 add_action('admin_menu', array($this, 'mxchat_add_plugin_page'));
41 - // Pro & Extensions registers at priority 30 so it always lands last in
42 - // the sidebar — below API Access (priority 20) and below any other
43 - // submenu that hooks at default priority 10.
44 - add_action('admin_menu', array($this, 'mxchat_add_pro_extensions_page'), 30);
45 - // Onboarding visibility — runs LAST so it can remove the submenu after every
46 - // other add_submenu_page() call. The page stays reachable by direct URL.
47 - add_action('admin_menu', array($this, 'mxchat_apply_onboarding_visibility'), 999);
48 41 add_action('admin_init', array($this, 'mxchat_page_init'));
49 42 add_action('admin_init', array($this, 'mxchat_prompts_page_init'));
50 43 add_action('admin_enqueue_scripts', array($this, 'mxchat_enqueue_admin_assets'));
51 - // Add body class for the Onboarding wizard (plan-905439) so the
52 - // chrome-surgery CSS in admin-onboarding-wizard.css can scope its
53 - // WP-sidebar collapse to this page only.
54 - add_filter('admin_body_class', array($this, 'mxchat_add_onboarding_body_class'));
55 44 add_action('wp_ajax_mxchat_delete_chat_history', array($this, 'mxchat_delete_chat_history'));
56 45 add_action('admin_post_mxchat_delete_prompt', array($this, 'mxchat_handle_delete_prompt'));
57 46 add_action('wp_ajax_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
58 47 add_action('wp_ajax_nopriv_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history'));
@@ -65,14 +54,8 @@
65 54 add_action('admin_post_mxchat_add_intent', array($this, 'mxchat_handle_add_intent'));
66 55 add_action('admin_post_mxchat_delete_intent', array($this, 'mxchat_handle_delete_intent'));
67 56 add_action('admin_post_mxchat_edit_intent', array($this, 'mxchat_handle_edit_intent'));
68 57 add_action('wp_ajax_mxchat_export_transcripts', array($this, 'export_chat_transcripts'));
69 -
70 - // Leads tab (inside Transcripts)
71 - add_action('wp_ajax_mxchat_fetch_leads', array($this, 'mxchat_fetch_leads'));
72 - add_action('wp_ajax_mxchat_delete_leads', array($this, 'mxchat_delete_leads'));
73 - add_action('wp_ajax_mxchat_export_leads', array($this, 'mxchat_export_leads'));
74 -
75 58 add_action('admin_init', array($this, 'mxchat_transcripts_page_init'));
76 59 add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice'));
77 60 add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice'));
78 61 add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts'));
@@ -96,12 +79,8 @@
96 79 add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions'));
97 80 add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax'));
98 81 add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax'));
99 82 add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax'));
100 - add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax'));
101 - add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax'));
102 - add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax'));
103 - add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax'));
104 83
105 84 // Slack test connection
106 85 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection'));
107 86
@@ -107,149 +86,16 @@
107 86
108 87 // Translation handlers
109 88 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages'));
110 89 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation'));
111 -
112 - // 3.2.3: Embedding model switch protection
113 - add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax'));
114 - add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax'));
115 - add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice'));
116 90 }
117 91
118 - /**
119 - * 3.2.3: Preflight check before allowing the embedding model dropdown to
120 - * switch. Pure option comparison — no counts, no DB queries beyond the
121 - * cached options. The dialog is shown whenever the user has previously
122 - * embedded with a different model than the one they're switching to.
123 - */
124 - public function mxchat_check_embedding_switch_ajax() {
125 - check_ajax_referer('mxchat_admin_nonce', 'security');
126 - if (!current_user_can('manage_options')) {
127 - wp_send_json_error(__('Unauthorized', 'mxchat'));
128 - }
129 -
130 - $new_model = isset($_POST['new_model']) ? sanitize_text_field(wp_unslash($_POST['new_model'])) : '';
131 - $active_model = MxChat_Utils::get_active_embedding_model();
132 -
133 - $is_mismatch = !empty($active_model) && !empty($new_model) && $active_model !== $new_model;
134 -
135 - $active_dims = MxChat_Utils::embedding_model_dimensions($active_model);
136 - $new_dims = MxChat_Utils::embedding_model_dimensions($new_model);
137 -
138 - wp_send_json_success(array(
139 - 'is_mismatch' => $is_mismatch,
140 - 'active_model' => $active_model,
141 - 'active_label' => MxChat_Utils::embedding_model_label($active_model),
142 - 'new_model' => $new_model,
143 - 'new_label' => MxChat_Utils::embedding_model_label($new_model),
144 - 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims),
145 - 'active_dims' => $active_dims,
146 - 'new_dims' => $new_dims,
147 - ));
148 - }
149 -
150 - /**
151 - * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected
152 - * pair so the banner reappears on the next switch event.
153 - */
154 - public function mxchat_dismiss_embedding_mismatch_ajax() {
155 - check_ajax_referer('mxchat_admin_nonce', 'security');
156 - if (!current_user_can('manage_options')) {
157 - wp_send_json_error(__('Unauthorized', 'mxchat'));
158 - }
159 -
160 - $options = get_option('mxchat_options', array());
161 - $selected = $options['embedding_model'] ?? '';
162 - $active = MxChat_Utils::get_active_embedding_model();
163 - update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false);
164 - wp_send_json_success();
165 - }
166 -
167 - /**
168 - * 3.2.3: Persistent admin banner shown whenever the active embedding model
169 - * (last used to actually embed something) differs from the currently
170 - * selected model. Pure option comparison — no DB queries on every page
171 - * load. The banner auto-clears once both match again, i.e. after a delete
172 - * + re-embed cycle.
173 - */
174 - public function mxchat_embedding_mismatch_notice() {
175 - if (!current_user_can('manage_options')) {
176 - return;
177 - }
178 -
179 - $options = get_option('mxchat_options', array());
180 - $selected = $options['embedding_model'] ?? '';
181 - $active = MxChat_Utils::get_active_embedding_model();
182 -
183 - if (empty($active) || empty($selected) || $active === $selected) {
184 - return;
185 - }
186 -
187 - $dismissed = get_option('mxchat_dismissed_embedding_mismatch', '');
188 - if ($dismissed === $active . '|' . $selected) {
189 - return;
190 - }
191 -
192 - $active_label = MxChat_Utils::embedding_model_label($active);
193 - $selected_label = MxChat_Utils::embedding_model_label($selected);
194 - $active_dims = MxChat_Utils::embedding_model_dimensions($active);
195 - $selected_dims = MxChat_Utils::embedding_model_dimensions($selected);
196 - $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims);
197 -
198 - $kb_url = admin_url('admin.php?page=mxchat-prompts');
199 - $actions_url = admin_url('admin.php?page=mxchat-actions');
200 -
201 - ?>
202 - <div class="notice notice-error is-dismissible mxchat-embedding-mismatch-notice"
203 - data-active="<?php echo esc_attr($active); ?>"
204 - data-selected="<?php echo esc_attr($selected); ?>">
205 - <p><strong><?php esc_html_e('MxChat: Embedding model mismatch detected', 'mxchat'); ?></strong></p>
206 - <p>
207 - <?php
208 - printf(
209 - /* translators: 1: previously-used model name, 2: currently-selected model name */
210 - esc_html__('Your knowledge base and actions were embedded with %1$s, but %2$s is now selected. Similarity matching will return inaccurate or empty results until you delete all existing embeddings and re-embed your content with the new model.', 'mxchat'),
211 - '<code>' . esc_html($active_label) . '</code>',
212 - '<code>' . esc_html($selected_label) . '</code>'
213 - );
214 - ?>
215 - </p>
216 - <?php if ($dims_differ) : ?>
217 - <p>
218 - <strong><?php esc_html_e('Dimension mismatch:', 'mxchat'); ?></strong>
219 - <?php
220 - printf(
221 - /* translators: 1: old dim count, 2: new dim count */
222 - esc_html__('Existing vectors are %1$d-dimensional but the new model produces %2$d-dimensional vectors. If you use Pinecone, your index will reject queries entirely until re-embedded.', 'mxchat'),
223 - (int) $active_dims,
224 - (int) $selected_dims
225 - );
226 - ?>
227 - </p>
228 - <?php endif; ?>
229 - <p>
230 - <?php esc_html_e('To fix this:', 'mxchat'); ?>
231 - <a href="<?php echo esc_url($kb_url); ?>"><?php esc_html_e('Delete all knowledge base entries', 'mxchat'); ?></a> ·
232 - <a href="<?php echo esc_url($actions_url); ?>"><?php esc_html_e('Delete all actions', 'mxchat'); ?></a> ·
233 - <?php esc_html_e('then re-import / re-add them with the new model selected.', 'mxchat'); ?>
234 - </p>
235 - </div>
236 - <script>
237 - (function($){
238 - $(document).on('click', '.mxchat-embedding-mismatch-notice .notice-dismiss', function(){
239 - $.post(ajaxurl, {
240 - action: 'mxchat_dismiss_embedding_mismatch',
241 - security: '<?php echo esc_js(wp_create_nonce('mxchat_admin_nonce')); ?>'
242 - });
243 - });
244 - })(jQuery);
245 - </script>
246 - <?php
247 - }
248 -
249 92 private function is_license_active() {
93 + // Get the raw value without translation
250 94 $license_status = get_option('mxchat_license_status', 'inactive');
251 - return ($license_status === 'active');
95 +
96 + // Check against multiple possible values, bypassing translation issues
97 + return ($license_status === 'active' || $license_status === esc_html__('active', 'mxchat'));
252 98 }
253 99
254 100 private function initialize_default_options() {
255 101 $default_options = array(
@@ -298,14 +144,9 @@
298 144 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude'
299 145 'post_type_visibility_list' => array(), // Array of post type slugs
300 146 'contextual_awareness_toggle' => 'off',
301 147 'citation_links_toggle' => 'on',
302 - 'satisfaction_rating_enabled' => 'off',
303 - 'satisfaction_rating_idle_seconds' => 60,
304 - 'satisfaction_rating_question' => '',
305 - 'satisfaction_rating_thanks' => '',
306 - 'satisfaction_rating_placeholder' => '',
307 - 'satisfaction_rating_saved' => '',
148 + 'show_frontend_debugger' => 'on',
308 149 'close_button_color' => esc_html__('#fff', 'mxchat'),
309 150 'chatbot_bg_color' => esc_html__('#fff', 'mxchat'),
310 151 'user_message_bg_color' => esc_html__('#fff', 'mxchat'),
311 152 'user_message_font_color' => esc_html__('#212121', 'mxchat'),
@@ -384,55 +225,26 @@
384 225 $this->options = $merged_options;
385 226 }
386 227
387 228 public function mxchat_add_plugin_page() {
388 - // Onboarding lifecycle helpers (admin_init redirect + ajax handlers live in the file).
389 - require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
390 -
391 - // Main menu page — `mxchat-max` remains the parent slug for every MxChat submenu
392 - // (Settings, Knowledge, Transcripts, …). Hitting `?page=mxchat-max` directly now
393 - // dispatches to the Onboarding page (or Settings if the user has dismissed onboarding).
229 + // Main menu page
394 230 add_menu_page(
231 + esc_html__('MxChat Settings', 'mxchat'),
395 232 esc_html__('MxChat', 'mxchat'),
396 - esc_html__('MxChat', 'mxchat'),
397 233 'manage_options',
398 234 'mxchat-max',
399 - array($this, 'mxchat_create_dashboard_page'),
235 + array($this, 'mxchat_create_admin_page'),
400 236 'dashicons-testimonial',
401 237 6
402 238 );
403 239
404 - // Onboarding submenu — first child under MxChat (plan-d14e89).
405 - // First registration uses menu_slug === parent slug 'mxchat-max' →
406 - // WP-canonical override of the auto-duplicate "MxChat" entry. Result: the
407 - // first child shows as "Onboarding" instead of a redundant pair.
240 + // Rename the first submenu item from "MxChat" to "Settings"
408 241 add_submenu_page(
409 242 'mxchat-max',
410 - esc_html__('MxChat Onboarding', 'mxchat'),
411 - esc_html__('Onboarding', 'mxchat'),
412 - 'manage_options',
413 - 'mxchat-max',
414 - array($this, 'mxchat_create_dashboard_page')
415 - );
416 - // Hidden route for `?page=mxchat-onboarding` (Settings "Show again" link
417 - // + legacy redirects still target this slug). Parent === null keeps it
418 - // out of the menu while remaining accessible by URL.
419 - add_submenu_page(
420 - null,
421 - esc_html__('MxChat Onboarding', 'mxchat'),
422 - esc_html__('Onboarding', 'mxchat'),
423 - 'manage_options',
424 - 'mxchat-onboarding',
425 - array($this, 'mxchat_create_dashboard_page')
426 - );
427 -
428 - // Settings submenu — same callback as before, just at a new slug.
429 - add_submenu_page(
430 - 'mxchat-max',
431 243 esc_html__('MxChat Settings', 'mxchat'),
432 244 esc_html__('Settings', 'mxchat'),
433 245 'manage_options',
434 - 'mxchat-settings',
246 + 'mxchat-max',
435 247 array($this, 'mxchat_create_admin_page')
436 248 );
437 249
438 250 // Submenu page for Knowledge
@@ -472,16 +284,9 @@
472 284 'mxchat-content',
473 285 array($this, 'mxchat_create_content_page')
474 286 );
475 287
476 -}
477 -
478 -/**
479 - * Register the Pro & Extensions submenu on a later admin_menu priority so it
480 - * always renders as the bottom-most item in the MxChat sidebar — below
481 - * configuration pages like API Access (priority 20).
482 - */
483 -public function mxchat_add_pro_extensions_page() {
288 + // Consolidated Pro & Extensions page (replaces separate Add Ons and Pro Upgrade pages)
484 289 add_submenu_page(
485 290 'mxchat-max',
486 291 esc_html__('Pro & Extensions', 'mxchat'),
487 292 esc_html__('Pro & Extensions', 'mxchat'),
@@ -1069,54 +874,8 @@
1069 874 </div>
1070 875 <?php
1071 876 }
1072 877 }
1073 -/**
1074 - * Render the Onboarding page. Delegates to the procedural renderer in
1075 - * includes/admin-onboarding-page.php. Wired to both `?page=mxchat-max`
1076 - * (legacy top-level URL) and `?page=mxchat-onboarding` (the canonical
1077 - * Onboarding submenu).
1078 - *
1079 - * When the user has dismissed onboarding and lands on `mxchat-max` (the
1080 - * legacy URL, since the Onboarding submenu has been removed), redirect to
1081 - * Settings instead — the page is "graduated" and we shouldn't dump them
1082 - * back onto it. They can still navigate here directly via the unhide link.
1083 - */
1084 -public function mxchat_create_dashboard_page() {
1085 - require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php';
1086 -
1087 - $current = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1088 - if ($current === 'mxchat-max' && function_exists('mxchat_onboarding_is_dismissed') && mxchat_onboarding_is_dismissed()) {
1089 - wp_safe_redirect(admin_url('admin.php?page=mxchat-settings'));
1090 - exit;
1091 - }
1092 -
1093 - if (function_exists('mxchat_render_onboarding_page')) {
1094 - mxchat_render_onboarding_page();
1095 - return;
1096 - }
1097 - // Defensive: if the include failed to load, fall back to the old Settings page
1098 - // so the top-level menu never lands on an empty screen.
1099 - $this->mxchat_create_admin_page();
1100 -}
1101 -
1102 -/**
1103 - * Hide the Onboarding submenu when the user has dismissed it (either
1104 - * manually or via auto-graduation). The page itself remains routable so
1105 - * the Settings "Show MxChat Onboarding again" link can navigate back to it.
1106 - */
1107 -public function mxchat_apply_onboarding_visibility() {
1108 - if (!function_exists('mxchat_onboarding_is_dismissed')) {
1109 - return;
1110 - }
1111 - if (mxchat_onboarding_is_dismissed()) {
1112 - // The first MxChat child is the same-slug-as-parent registration
1113 - // (slug 'mxchat-max', labelled "Onboarding") added in plan-d14e89.
1114 - // Remove it so the menu opens straight to Settings after dismiss.
1115 - remove_submenu_page('mxchat-max', 'mxchat-max');
1116 - }
1117 -}
1118 -
1119 878 public function mxchat_create_admin_page() {
1120 879 $this->add_live_agent_nonce();
1121 880 $this->add_theme_migration_nonce();
1122 881
@@ -1358,11 +1117,8 @@
1358 1117 $chart_messages[] = 0;
1359 1118 }
1360 1119 }
1361 1120
1362 - // Satisfaction rating rollup — last 30 days, grouped by bot (plan-a5b006).
1363 - $satisfaction_stats = $this->get_satisfaction_rating_stats(30);
1364 -
1365 1121 // Prepare page data for the template
1366 1122 $page_data = array(
1367 1123 'total_chats' => $total_chats,
1368 1124 'total_messages' => $total_messages,
@@ -1377,9 +1133,8 @@
1377 1133 'busiest_hour' => $busiest_hour,
1378 1134 'chart_labels' => $chart_labels,
1379 1135 'chart_chats' => $chart_chats,
1380 1136 'chart_messages' => $chart_messages,
1381 - 'satisfaction_stats' => $satisfaction_stats,
1382 1137 );
1383 1138
1384 1139 // Include and render the new template
1385 1140 require_once plugin_dir_path(__FILE__) . 'admin-transcripts-page.php';
@@ -1386,50 +1141,8 @@
1386 1141 mxchat_render_transcripts_page($this, $page_data);
1387 1142 }
1388 1143
1389 1144 /**
1390 - * Per-bot satisfaction rating rollup over the last $days days. Used by the
1391 - * Satisfaction card on the Transcripts dashboard (plan-a5b006).
1392 - *
1393 - * @param int $days Window in days.
1394 - * @return array Each entry: ['bot_id', 'total', 'positive', 'negative', 'positive_pct', 'negative_pct'].
1395 - */
1396 -public function get_satisfaction_rating_stats($days = 30) {
1397 - global $wpdb;
1398 - $table = $wpdb->prefix . 'mxchat_session_ratings';
1399 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
1400 - return array();
1401 - }
1402 - $days = max(1, (int) $days);
1403 - $rows = $wpdb->get_results($wpdb->prepare(
1404 - "SELECT bot_id,
1405 - COUNT(*) AS total,
1406 - SUM(CASE WHEN rating_value = 1 THEN 1 ELSE 0 END) AS positive,
1407 - SUM(CASE WHEN rating_value = -1 THEN 1 ELSE 0 END) AS negative
1408 - FROM {$table}
1409 - WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
1410 - GROUP BY bot_id
1411 - ORDER BY total DESC",
1412 - $days
1413 - ));
1414 - $out = array();
1415 - foreach ((array) $rows as $row) {
1416 - $total = (int) $row->total;
1417 - $positive = (int) $row->positive;
1418 - $negative = (int) $row->negative;
1419 - $out[] = array(
1420 - 'bot_id' => $row->bot_id ?: 'default',
1421 - 'total' => $total,
1422 - 'positive' => $positive,
1423 - 'negative' => $negative,
1424 - 'positive_pct' => $total > 0 ? (int) round(($positive / $total) * 100) : 0,
1425 - 'negative_pct' => $total > 0 ? (int) round(($negative / $total) * 100) : 0,
1426 - );
1427 - }
1428 - return $out;
1429 -}
1430 -
1431 -/**
1432 1145 * Get chart data for transcripts page
1433 1146 * Used by both page render and script localization
1434 1147 *
1435 1148 * @return array Chart data with labels, chats, and messages arrays
@@ -1541,39 +1254,8 @@
1541 1254 </p>
1542 1255 <?php
1543 1256 }
1544 1257
1545 -/**
1546 - * Custom retention-days input. When > 0 it overrides the bucket dropdown above
1547 - * and deletes transcripts older than the given number of days. Set to 0 to fall
1548 - * back to the dropdown (or "Never" if the dropdown is also Never).
1549 - *
1550 - * Devs can override the final day count via the `mxchat_transcript_retention_days`
1551 - * filter — runs in `cleanup_old_transcripts()` after this option is read.
1552 - *
1553 - * (plan-mxchat-20260509-9b80b1)
1554 - */
1555 -public function mxchat_retention_days_callback() {
1556 - $options = get_option('mxchat_transcripts_options', array());
1557 - $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1558 - ?>
1559 - <input type="number"
1560 - id="mxchat_retention_days"
1561 - name="mxchat_transcripts_options[mxchat_retention_days]"
1562 - value="<?php echo esc_attr($days); ?>"
1563 - min="0"
1564 - max="3650"
1565 - step="1"
1566 - style="width: 90px;" />
1567 - <p class="description">
1568 - <?php esc_html_e('Number of days to retain transcripts. When set to a value greater than 0, this overrides the dropdown above. Set to 0 to use the dropdown. Maximum 3650 (10 years). A daily wp-cron task removes anything older, cascading to translations and click-tracking rows.', 'mxchat'); ?>
1569 - <br>
1570 - <code>apply_filters( 'mxchat_transcript_retention_days', $days )</code>
1571 - <?php esc_html_e('lets developers override the final day count programmatically.', 'mxchat'); ?>
1572 - </p>
1573 - <?php
1574 -}
1575 -
1576 1258 public function mxchat_auto_email_transcript_callback() {
1577 1259 $options = get_option('mxchat_transcripts_options', array());
1578 1260 $enabled = isset($options['mxchat_auto_email_transcript_enabled']) ? $options['mxchat_auto_email_transcript_enabled'] : 0;
1579 1261 $delay = isset($options['mxchat_auto_email_transcript_delay']) ? $options['mxchat_auto_email_transcript_delay'] : '30';
@@ -1649,29 +1331,15 @@
1649 1331 } else {
1650 1332 $sanitized['mxchat_auto_delete_transcripts'] = 'never';
1651 1333 }
1652 1334
1653 - // Sanitize custom retention-days override (plan-9b80b1).
1654 - if (isset($input['mxchat_retention_days'])) {
1655 - $days = (int) $input['mxchat_retention_days'];
1656 - $sanitized['mxchat_retention_days'] = max(0, min(3650, $days));
1657 - } else {
1658 - $sanitized['mxchat_retention_days'] = 0;
1659 - }
1660 -
1661 - // Get old values to check if auto-delete or retention-days changed.
1335 + // Get old value to check if auto-delete setting changed
1662 1336 $old_options = get_option('mxchat_transcripts_options');
1663 1337 $old_interval = isset($old_options['mxchat_auto_delete_transcripts']) ? $old_options['mxchat_auto_delete_transcripts'] : 'never';
1664 - $old_retention = isset($old_options['mxchat_retention_days']) ? (int) $old_options['mxchat_retention_days'] : 0;
1665 -
1666 - // If either setting changed, reschedule the cron job. The schedule_transcript_cleanup
1667 - // helper now treats "any active retention" (dropdown != never OR custom days > 0) as a
1668 - // reason to keep the daily cron registered.
1669 - $interval_changed = ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']);
1670 - $retention_changed = ($old_retention !== $sanitized['mxchat_retention_days']);
1671 - if ($interval_changed || $retention_changed) {
1672 - $any_active = ($sanitized['mxchat_auto_delete_transcripts'] !== 'never') || ($sanitized['mxchat_retention_days'] > 0);
1673 - $this->schedule_transcript_cleanup($any_active ? 'active' : 'never');
1338 +
1339 + // If the auto-delete setting changed, reschedule the cron job
1340 + if ($old_interval !== $sanitized['mxchat_auto_delete_transcripts']) {
1341 + $this->schedule_transcript_cleanup($sanitized['mxchat_auto_delete_transcripts']);
1674 1342 }
1675 1343
1676 1344 // Sanitize auto-email transcript settings
1677 1345 $sanitized['mxchat_auto_email_transcript_enabled'] = isset($input['mxchat_auto_email_transcript_enabled']) ? 1 : 0;
@@ -1699,13 +1367,10 @@
1699 1367 $timestamp = wp_next_scheduled('mxchat_cleanup_old_transcripts');
1700 1368 if ($timestamp) {
1701 1369 wp_unschedule_event($timestamp, 'mxchat_cleanup_old_transcripts');
1702 1370 }
1703 -
1704 - // Schedule new event whenever retention is active. "active" is the canonical
1705 - // value passed by sanitize_transcripts_options when either the dropdown != never
1706 - // OR the custom retention-days > 0; "never" turns the cron off. Any other value
1707 - // (the legacy "1week" / "2weeks" / "1month" strings) is also treated as active.
1371 +
1372 + // Schedule new event if not set to "never"
1708 1373 if ($interval !== 'never') {
1709 1374 // Schedule to run daily at 3 AM
1710 1375 $next_run = strtotime('tomorrow 3:00 AM');
1711 1376 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
@@ -1715,74 +1380,58 @@
1715 1380 /**
1716 1381 * Delete old transcripts based on the configured interval
1717 1382 */
1718 1383 public function cleanup_old_transcripts() {
1719 - $options = get_option('mxchat_transcripts_options', array());
1720 - $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1721 - $custom_days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0;
1722 -
1723 - // Custom retention-days (plan-9b80b1) takes precedence over the bucket dropdown.
1384 + $options = get_option('mxchat_transcripts_options', array());
1385 + $interval = isset($options['mxchat_auto_delete_transcripts']) ? $options['mxchat_auto_delete_transcripts'] : 'never';
1386 +
1387 + // If set to never, don't delete anything
1388 + if ($interval === 'never') {
1389 + return;
1390 + }
1391 +
1392 + // Calculate the cutoff date
1724 1393 $days = 0;
1725 - if ($custom_days > 0) {
1726 - $days = $custom_days;
1727 - } else {
1728 - switch ($interval) {
1729 - case '1week': $days = 7; break;
1730 - case '2weeks': $days = 14; break;
1731 - case '1month': $days = 30; break;
1732 - case 'never':
1733 - default:
1734 - $days = 0;
1735 - }
1394 + switch ($interval) {
1395 + case '1week':
1396 + $days = 7;
1397 + break;
1398 + case '2weeks':
1399 + $days = 14;
1400 + break;
1401 + case '1month':
1402 + $days = 30;
1403 + break;
1404 + default:
1405 + return; // Invalid interval, don't delete anything
1736 1406 }
1737 -
1738 - // Devs can override the final day count programmatically.
1739 - $days = (int) apply_filters('mxchat_transcript_retention_days', $days);
1740 -
1741 - if ($days <= 0) {
1742 - return; // Retention disabled — bail.
1743 - }
1744 -
1407 +
1745 1408 global $wpdb;
1746 - $transcripts_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1409 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1747 1410 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
1748 - $url_clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
1749 1411
1750 - // Defensive: if the main table doesn't exist (fresh-ish install), bail.
1751 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $transcripts_table)) !== $transcripts_table) {
1752 - return;
1753 - }
1412 + // Calculate the cutoff timestamp
1413 + $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
1754 1414
1755 - $cutoff_date = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
1756 -
1757 - // Cap at 5000 session_ids per run so a large unattended site doesn't OOM —
1758 - // the cron will pick up where it left off on the next tick (within hours).
1759 - $batch_cap = (int) apply_filters('mxchat_transcript_retention_batch_cap', 5000);
1760 -
1415 + // First, get the session IDs that will be deleted (to clean up translations too)
1761 1416 $sessions_to_delete = $wpdb->get_col(
1762 1417 $wpdb->prepare(
1763 - "SELECT DISTINCT session_id FROM {$transcripts_table} WHERE timestamp IS NOT NULL AND timestamp < %s LIMIT %d",
1764 - $cutoff_date,
1765 - $batch_cap
1418 + "SELECT DISTINCT session_id FROM {$table_name} WHERE timestamp < %s",
1419 + $cutoff_date
1766 1420 )
1767 1421 );
1768 1422
1769 - if (empty($sessions_to_delete)) {
1770 - return;
1771 - }
1772 -
1773 - $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1774 -
1775 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1776 - // $placeholders is a server-built list of literal "%s" tokens.
1777 - $deleted_transcripts = (int) $wpdb->query(
1423 + // Delete transcripts older than the cutoff date
1424 + $deleted = $wpdb->query(
1778 1425 $wpdb->prepare(
1779 - "DELETE FROM {$transcripts_table} WHERE session_id IN ($placeholders)",
1780 - $sessions_to_delete
1426 + "DELETE FROM {$table_name} WHERE timestamp < %s",
1427 + $cutoff_date
1781 1428 )
1782 1429 );
1783 1430
1784 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $translations_table)) === $translations_table) {
1431 + // Also delete any translations for the deleted sessions
1432 + if (!empty($sessions_to_delete) && $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) {
1433 + $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s'));
1785 1434 $wpdb->query(
1786 1435 $wpdb->prepare(
1787 1436 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)",
1788 1437 $sessions_to_delete
@@ -1789,20 +1438,12 @@
1789 1438 )
1790 1439 );
1791 1440 }
1792 1441
1793 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $url_clicks_table)) === $url_clicks_table) {
1794 - $wpdb->query(
1795 - $wpdb->prepare(
1796 - "DELETE FROM {$url_clicks_table} WHERE session_id IN ($placeholders)",
1797 - $sessions_to_delete
1798 - )
1799 - );
1442 + // Log the cleanup action
1443 + if ($deleted !== false && $deleted > 0) {
1444 + //error_log(sprintf('MXChat: Auto-deleted %d transcripts older than %d days', $deleted, $days));
1800 1445 }
1801 - // phpcs:enable
1802 -
1803 - update_option('mxchat_retention_last_swept_at', time(), false);
1804 - update_option('mxchat_retention_rows_last_deleted', $deleted_transcripts, false);
1805 1446 }
1806 1447
1807 1448 public function export_chat_transcripts() {
1808 1449 if (!current_user_can('manage_options')) {
@@ -1863,763 +1504,9 @@
1863 1504 fclose($output);
1864 1505 wp_die();
1865 1506 }
1866 1507
1867 -// ============================================================================
1868 -// Leads tab (inside Transcripts)
1869 -//
1870 -// Leads are derived from existing data — no dedicated table. Primary source:
1871 -// wp_mxchat_chat_transcripts rows where user_email is populated. Secondary
1872 -// source: wp_options entries `mxchat_email_{session_id}` / `mxchat_name_{sid}`
1873 -// for "orphan" leads who submitted the pre-chat form but never chatted.
1874 -// ============================================================================
1875 -
1876 1508 /**
1877 - * Fetch leads: dedup-by-email rows, stats strip, and top pages in one call.
1878 - */
1879 -public function mxchat_fetch_leads() {
1880 - if (!current_user_can('manage_options')) {
1881 - wp_send_json_error(['message' => 'Insufficient permissions']);
1882 - wp_die();
1883 - }
1884 -
1885 - global $wpdb;
1886 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
1887 -
1888 - $page = isset($_POST['page']) ? max(1, absint($_POST['page'])) : 1;
1889 - $per_page = isset($_POST['per_page']) ? min(100, max(10, absint($_POST['per_page']))) : 25;
1890 - $offset = ($page - 1) * $per_page;
1891 - $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1892 - $date_range = isset($_POST['date_range']) ? sanitize_key($_POST['date_range']) : 'all';
1893 - $status = isset($_POST['status']) ? sanitize_key($_POST['status']) : 'all';
1894 - $page_filter = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : '';
1895 - $sort = isset($_POST['sort']) ? sanitize_key($_POST['sort']) : 'last_seen';
1896 - $sort_dir = (isset($_POST['sort_dir']) && $_POST['sort_dir'] === 'asc') ? 'ASC' : 'DESC';
1897 -
1898 - $date_cutoff = self::mxchat_leads_date_cutoff($date_range);
1899 - $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
1900 -
1901 - // Base WHERE for transcripts leads.
1902 - $where_clauses = ["user_email IS NOT NULL", "user_email != ''"];
1903 - $where_params = [];
1904 -
1905 - if ($date_cutoff) {
1906 - $where_clauses[] = 'timestamp >= %s';
1907 - $where_params[] = $date_cutoff;
1908 - }
1909 - if ($page_filter && $has_page_url_column) {
1910 - $where_clauses[] = 'originating_page_url = %s';
1911 - $where_params[] = $page_filter;
1912 - }
1913 - if ($search !== '') {
1914 - $like = '%' . $wpdb->esc_like($search) . '%';
1915 - $where_clauses[] = '(user_email LIKE %s OR user_name LIKE %s)';
1916 - $where_params[] = $like;
1917 - $where_params[] = $like;
1918 - }
1919 - $where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
1920 -
1921 - // Aggregate query grouped by email.
1922 - $select_sql = $has_page_url_column
1923 - ? "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1924 - COUNT(DISTINCT session_id) AS conversation_count"
1925 - : "SELECT user_email, MAX(timestamp) AS last_seen, MIN(timestamp) AS first_seen,
1926 - COUNT(DISTINCT session_id) AS conversation_count";
1927 -
1928 - $order_column = in_array($sort, ['last_seen', 'conversation_count', 'first_seen'], true) ? $sort : 'last_seen';
1929 - $group_order_limit = " GROUP BY user_email ORDER BY {$order_column} {$sort_dir} LIMIT %d OFFSET %d";
1930 -
1931 - $transcripts_sql = $wpdb->prepare(
1932 - "{$select_sql} FROM {$table} {$where_sql}{$group_order_limit}",
1933 - array_merge($where_params, [$per_page, $offset])
1934 - );
1935 - $transcript_rows = $wpdb->get_results($transcripts_sql);
1936 -
1937 - // Count of unique transcript-based leads under the same filters.
1938 - $count_sql = $wpdb->prepare(
1939 - "SELECT COUNT(DISTINCT user_email) FROM {$table} {$where_sql}",
1940 - $where_params
1941 - );
1942 - $transcripts_lead_count = (int) $wpdb->get_var($count_sql);
1943 -
1944 - // Hydrate each row: name, latest_session_id, top page.
1945 - $leads = [];
1946 - foreach ($transcript_rows as $row) {
1947 - $detail = $has_page_url_column
1948 - ? $wpdb->get_row($wpdb->prepare(
1949 - "SELECT session_id, user_name, originating_page_url, originating_page_title
1950 - FROM {$table} WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1951 - $row->user_email
1952 - ))
1953 - : $wpdb->get_row($wpdb->prepare(
1954 - "SELECT session_id, user_name FROM {$table}
1955 - WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
1956 - $row->user_email
1957 - ));
1958 -
1959 - $leads[] = [
1960 - 'email' => $row->user_email,
1961 - 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
1962 - 'conversation_count' => (int) $row->conversation_count,
1963 - 'last_seen' => $row->last_seen,
1964 - 'last_seen_display' => self::mxchat_leads_format_relative($row->last_seen),
1965 - 'first_seen' => $row->first_seen,
1966 - 'latest_session_id' => isset($detail->session_id) ? $detail->session_id : '',
1967 - 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
1968 - 'top_page_title' => isset($detail->originating_page_title) ? $detail->originating_page_title : '',
1969 - 'is_orphan' => false,
1970 - 'status' => 'active',
1971 - ];
1972 - }
1973 -
1974 - // Non-transcript lead sources. Built once here, then filtered/merged based on the
1975 - // status filter below. Deduplication priority when the same email appears in multiple
1976 - // sources: transcripts > chat_deleted > orphan.
1977 - $transcripts_emails_seen = array_flip(array_map(
1978 - function ($r) { return strtolower($r['email']); },
1979 - $leads
1980 - ));
1981 -
1982 - // Chat-deleted leads: had a conversation that an admin removed. Preserved via
1983 - // mxchat_lead_del_* options, with timestamps so they respect date filters.
1984 - $chat_deleted_leads_all = [];
1985 - if ($status === 'all' || $status === 'chat_deleted') {
1986 - $chat_deleted_leads_all = self::mxchat_collect_chat_deleted_leads($search, $date_cutoff);
1987 - // Dedup: drop any chat_deleted row whose email is already in the transcripts set.
1988 - $chat_deleted_leads_all = array_values(array_filter(
1989 - $chat_deleted_leads_all,
1990 - function ($row) use ($transcripts_emails_seen) {
1991 - return !isset($transcripts_emails_seen[strtolower($row['email'])]);
1992 - }
1993 - ));
1994 - foreach ($chat_deleted_leads_all as $row) {
1995 - $transcripts_emails_seen[strtolower($row['email'])] = true;
1996 - }
1997 - }
1998 -
1999 - // Orphans: pre-chat form captures with no conversation. No timestamp, so skipped
2000 - // when a date filter is active.
2001 - $orphan_leads_all = [];
2002 - if (($status === 'all' || $status === 'orphan') && !$date_cutoff && !$page_filter) {
2003 - $orphan_leads_all = self::mxchat_collect_orphan_leads($search);
2004 - $orphan_leads_all = array_values(array_filter(
2005 - $orphan_leads_all,
2006 - function ($row) use ($transcripts_emails_seen) {
2007 - return !isset($transcripts_emails_seen[strtolower($row['email'])]);
2008 - }
2009 - ));
2010 - }
2011 -
2012 - // Apply status filter to the transcripts-derived list.
2013 - if ($status === 'orphan' || $status === 'chat_deleted') {
2014 - $leads = [];
2015 - $transcripts_lead_count = 0;
2016 - }
2017 -
2018 - // Stitch the current page from the three buckets in priority order.
2019 - $total_count = $transcripts_lead_count + count($chat_deleted_leads_all) + count($orphan_leads_all);
2020 - $remaining_slots = $per_page - count($leads);
2021 -
2022 - if ($remaining_slots > 0 && !empty($chat_deleted_leads_all)) {
2023 - $start = max(0, ($page - 1) * $per_page - $transcripts_lead_count);
2024 - if ($start < count($chat_deleted_leads_all)) {
2025 - $leads = array_merge($leads, array_slice($chat_deleted_leads_all, $start, $remaining_slots));
2026 - $remaining_slots = $per_page - count($leads);
2027 - }
2028 - }
2029 -
2030 - if ($remaining_slots > 0 && !empty($orphan_leads_all)) {
2031 - $before = $transcripts_lead_count + count($chat_deleted_leads_all);
2032 - $start = max(0, ($page - 1) * $per_page - $before);
2033 - if ($start < count($orphan_leads_all)) {
2034 - $leads = array_merge($leads, array_slice($orphan_leads_all, $start, $remaining_slots));
2035 - }
2036 - }
2037 -
2038 - $total_pages = $per_page > 0 ? (int) ceil($total_count / $per_page) : 1;
2039 -
2040 - // Stats strip: always computed over full dataset, unaffected by filters.
2041 - $stats = self::mxchat_leads_stats($table, $has_page_url_column);
2042 -
2043 - // Top pages: top 5 by distinct emails captured.
2044 - $top_pages = [];
2045 - if ($has_page_url_column) {
2046 - $top_pages_rows = $wpdb->get_results(
2047 - "SELECT originating_page_url AS url,
2048 - MAX(originating_page_title) AS title,
2049 - COUNT(DISTINCT user_email) AS lead_count
2050 - FROM {$table}
2051 - WHERE user_email IS NOT NULL AND user_email != ''
2052 - AND originating_page_url IS NOT NULL AND originating_page_url != ''
2053 - GROUP BY originating_page_url
2054 - ORDER BY lead_count DESC, url ASC
2055 - LIMIT 5"
2056 - );
2057 - foreach ($top_pages_rows as $p) {
2058 - $top_pages[] = [
2059 - 'url' => $p->url,
2060 - 'title' => $p->title ?: $p->url,
2061 - 'lead_count' => (int) $p->lead_count,
2062 - ];
2063 - }
2064 - }
2065 -
2066 - wp_send_json([
2067 - 'success' => true,
2068 - 'leads' => $leads,
2069 - 'page' => $page,
2070 - 'per_page' => $per_page,
2071 - 'total_count' => $total_count,
2072 - 'total_pages' => $total_pages,
2073 - 'showing_start' => $total_count === 0 ? 0 : ($offset + 1),
2074 - 'showing_end' => min($offset + $per_page, $total_count),
2075 - 'stats' => $stats,
2076 - 'top_pages' => $top_pages,
2077 - ]);
2078 - wp_die();
2079 -}
2080 -
2081 -/**
2082 - * Delete one or more leads by email. Removes every transcripts row for that
2083 - * email and cleans up related wp_options (mxchat_email_{sid}, mxchat_name_{sid},
2084 - * mxchat_history_{sid}) and any orphan option entries matching the email.
2085 - */
2086 -public function mxchat_delete_leads() {
2087 - if (!current_user_can('manage_options')) {
2088 - wp_send_json_error(['message' => 'Insufficient permissions']);
2089 - wp_die();
2090 - }
2091 - check_ajax_referer('mxchat_delete_leads', 'security');
2092 -
2093 - $emails_raw = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2094 - $emails = [];
2095 - foreach ($emails_raw as $e) {
2096 - $clean = sanitize_email((string) $e);
2097 - if ($clean) {
2098 - $emails[] = $clean;
2099 - }
2100 - }
2101 - if (empty($emails)) {
2102 - wp_send_json_error(['message' => 'No emails provided']);
2103 - wp_die();
2104 - }
2105 -
2106 - $summary = self::mxchat_wipe_leads_by_email($emails);
2107 -
2108 - wp_send_json([
2109 - 'success' => true,
2110 - 'deleted_leads' => count($emails),
2111 - 'deleted_sessions' => $summary['deleted_sessions'],
2112 - 'deleted_rows' => $summary['deleted_rows'],
2113 - ]);
2114 - wp_die();
2115 -}
2116 -
2117 -/**
2118 - * Fully wipe one or more leads by email: every transcripts row, every related wp_options
2119 - * entry (history, pre-chat capture, chat_deleted preservation, agent name, translations).
2120 - *
2121 - * Shared between the Leads-tab Delete button and the transcript-delete opt-in checkbox.
2122 - * Input emails must already be sanitized with sanitize_email().
2123 - */
2124 -private static function mxchat_wipe_leads_by_email(array $emails) {
2125 - global $wpdb;
2126 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2127 - $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
2128 - $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
2129 -
2130 - $deleted_sessions = 0;
2131 - $deleted_rows = 0;
2132 - $emails_lc = array_map('strtolower', $emails);
2133 -
2134 - foreach ($emails as $email) {
2135 - $session_ids = $wpdb->get_col($wpdb->prepare(
2136 - "SELECT DISTINCT session_id FROM {$table} WHERE user_email = %s",
2137 - $email
2138 - ));
2139 -
2140 - $rows_removed = $wpdb->delete($table, ['user_email' => $email], ['%s']);
2141 - if ($rows_removed !== false) {
2142 - $deleted_rows += (int) $rows_removed;
2143 - }
2144 -
2145 - foreach ($session_ids as $sid) {
2146 - $deleted_sessions++;
2147 - wp_cache_delete('chat_session_' . $sid, 'mxchat_chat_sessions');
2148 - delete_option('mxchat_history_' . $sid);
2149 - delete_option('mxchat_email_' . $sid);
2150 - delete_option('mxchat_name_' . $sid);
2151 - delete_option('mxchat_agent_name_' . $sid);
2152 - delete_option('mxchat_lead_del_email_' . $sid);
2153 - delete_option('mxchat_lead_del_name_' . $sid);
2154 - delete_option('mxchat_lead_del_ts_' . $sid);
2155 - if ($has_translations) {
2156 - $wpdb->delete($translations_table, ['session_id' => $sid], ['%s']);
2157 - }
2158 - }
2159 - }
2160 -
2161 - // Clean up any lingering option entries (orphan pre-chat captures + chat_deleted
2162 - // preservations) whose stored value matches one of the emails being wiped.
2163 - $lingering = $wpdb->get_results(
2164 - "SELECT option_name, option_value FROM {$wpdb->options}
2165 - WHERE option_name LIKE 'mxchat_email_%' OR option_name LIKE 'mxchat_lead_del_email_%'"
2166 - );
2167 - foreach ($lingering as $opt) {
2168 - if (!in_array(strtolower(trim($opt->option_value)), $emails_lc, true)) {
2169 - continue;
2170 - }
2171 - if (strpos($opt->option_name, 'mxchat_lead_del_email_') === 0) {
2172 - $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2173 - delete_option('mxchat_lead_del_email_' . $sid);
2174 - delete_option('mxchat_lead_del_name_' . $sid);
2175 - delete_option('mxchat_lead_del_ts_' . $sid);
2176 - } else {
2177 - $sid = substr($opt->option_name, strlen('mxchat_email_'));
2178 - delete_option('mxchat_email_' . $sid);
2179 - delete_option('mxchat_name_' . $sid);
2180 - }
2181 - }
2182 -
2183 - wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
2184 -
2185 - return [
2186 - 'deleted_sessions' => $deleted_sessions,
2187 - 'deleted_rows' => $deleted_rows,
2188 - ];
2189 -}
2190 -
2191 -/**
2192 - * Stream a leads CSV. scope=all exports every lead under current filters is not
2193 - * supported to keep semantics simple; caller either exports all leads or a
2194 - * specific set of selected emails.
2195 - */
2196 -public function mxchat_export_leads() {
2197 - if (!current_user_can('manage_options')) {
2198 - wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'mxchat'));
2199 - }
2200 - check_ajax_referer('mxchat_export_leads', 'security');
2201 -
2202 - $scope = isset($_POST['scope']) ? sanitize_key($_POST['scope']) : 'all';
2203 - $fields_mode = isset($_POST['fields']) ? sanitize_key($_POST['fields']) : 'email_and_name';
2204 - $emails_in = isset($_POST['emails']) ? (array) wp_unslash($_POST['emails']) : [];
2205 -
2206 - $emails_in_clean = [];
2207 - foreach ($emails_in as $e) {
2208 - $clean = sanitize_email((string) $e);
2209 - if ($clean) {
2210 - $emails_in_clean[] = $clean;
2211 - }
2212 - }
2213 -
2214 - global $wpdb;
2215 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2216 - $has_page_url_column = !empty($wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'originating_page_url'"));
2217 -
2218 - // Collect leads from transcripts.
2219 - $transcripts_sql = "SELECT user_email AS email,
2220 - MAX(timestamp) AS last_seen,
2221 - COUNT(DISTINCT session_id) AS conversation_count
2222 - FROM {$table}
2223 - WHERE user_email IS NOT NULL AND user_email != ''";
2224 - $params = [];
2225 - if ($scope === 'selected' && !empty($emails_in_clean)) {
2226 - $placeholders = implode(',', array_fill(0, count($emails_in_clean), '%s'));
2227 - $transcripts_sql .= " AND user_email IN ({$placeholders})";
2228 - $params = $emails_in_clean;
2229 - }
2230 - $transcripts_sql .= " GROUP BY user_email ORDER BY last_seen DESC";
2231 -
2232 - $rows = !empty($params)
2233 - ? $wpdb->get_results($wpdb->prepare($transcripts_sql, $params))
2234 - : $wpdb->get_results($transcripts_sql);
2235 -
2236 - // Hydrate each row with name + top page.
2237 - $export_rows = [];
2238 - foreach ($rows as $row) {
2239 - $detail = $has_page_url_column
2240 - ? $wpdb->get_row($wpdb->prepare(
2241 - "SELECT user_name, originating_page_url FROM {$table}
2242 - WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2243 - $row->email
2244 - ))
2245 - : $wpdb->get_row($wpdb->prepare(
2246 - "SELECT user_name FROM {$table}
2247 - WHERE user_email = %s ORDER BY timestamp DESC LIMIT 1",
2248 - $row->email
2249 - ));
2250 - $export_rows[] = [
2251 - 'email' => $row->email,
2252 - 'name' => isset($detail->user_name) ? (string) $detail->user_name : '',
2253 - 'conversation_count' => (int) $row->conversation_count,
2254 - 'last_seen' => $row->last_seen,
2255 - 'top_page_url' => isset($detail->originating_page_url) ? $detail->originating_page_url : '',
2256 - ];
2257 - }
2258 -
2259 - // Include orphan + chat_deleted leads when exporting all.
2260 - if ($scope === 'all') {
2261 - $transcripts_emails_lc = array_flip(array_map(
2262 - function ($r) { return strtolower($r['email']); },
2263 - $export_rows
2264 - ));
2265 - foreach (self::mxchat_collect_chat_deleted_leads('') as $cd) {
2266 - if (isset($transcripts_emails_lc[strtolower($cd['email'])])) continue;
2267 - $transcripts_emails_lc[strtolower($cd['email'])] = true;
2268 - $export_rows[] = [
2269 - 'email' => $cd['email'],
2270 - 'name' => $cd['name'],
2271 - 'conversation_count' => 0,
2272 - 'last_seen' => $cd['last_seen'],
2273 - 'top_page_url' => '',
2274 - ];
2275 - }
2276 - foreach (self::mxchat_collect_orphan_leads('') as $orphan) {
2277 - if (isset($transcripts_emails_lc[strtolower($orphan['email'])])) continue;
2278 - $transcripts_emails_lc[strtolower($orphan['email'])] = true;
2279 - $export_rows[] = [
2280 - 'email' => $orphan['email'],
2281 - 'name' => $orphan['name'],
2282 - 'conversation_count' => 0,
2283 - 'last_seen' => '',
2284 - 'top_page_url' => '',
2285 - ];
2286 - }
2287 - }
2288 -
2289 - if (empty($export_rows)) {
2290 - wp_send_json_error(['message' => 'No leads to export.']);
2291 - wp_die();
2292 - }
2293 -
2294 - $filename = 'mxchat-leads-' . date('Y-m-d') . '.csv';
2295 - header('Content-Type: text/csv');
2296 - header('Content-Disposition: attachment; filename="' . $filename . '"');
2297 - header('Pragma: no-cache');
2298 - header('Expires: 0');
2299 -
2300 - $output = fopen('php://output', 'w');
2301 - fputs($output, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel
2302 -
2303 - if ($fields_mode === 'email_only') {
2304 - fputcsv($output, ['Email']);
2305 - foreach ($export_rows as $r) {
2306 - fputcsv($output, [$r['email']]);
2307 - }
2308 - } else {
2309 - fputcsv($output, ['Email', 'Name', 'Conversations', 'Last seen', 'Top page']);
2310 - foreach ($export_rows as $r) {
2311 - fputcsv($output, [
2312 - $r['email'],
2313 - $r['name'],
2314 - $r['conversation_count'],
2315 - $r['last_seen'],
2316 - $r['top_page_url'],
2317 - ]);
2318 - }
2319 - }
2320 -
2321 - fclose($output);
2322 - wp_die();
2323 -}
2324 -
2325 -/**
2326 - * Stats strip payload (independent of filters).
2327 - */
2328 -private static function mxchat_leads_stats($table, $has_page_url_column) {
2329 - global $wpdb;
2330 -
2331 - $total_transcripts_emails = (int) $wpdb->get_var(
2332 - "SELECT COUNT(DISTINCT user_email) FROM {$table}
2333 - WHERE user_email IS NOT NULL AND user_email != ''"
2334 - );
2335 -
2336 - $new_this_week = (int) $wpdb->get_var($wpdb->prepare(
2337 - "SELECT COUNT(*) FROM (
2338 - SELECT user_email FROM {$table}
2339 - WHERE user_email IS NOT NULL AND user_email != ''
2340 - GROUP BY user_email
2341 - HAVING MIN(timestamp) >= %s
2342 - ) AS new_leads",
2343 - gmdate('Y-m-d H:i:s', strtotime('-7 days'))
2344 - ));
2345 -
2346 - $total_convos = (int) $wpdb->get_var(
2347 - "SELECT COUNT(DISTINCT session_id) FROM {$table}
2348 - WHERE user_email IS NOT NULL AND user_email != ''"
2349 - );
2350 -
2351 - $orphan_count = count(self::mxchat_collect_orphan_leads(''));
2352 - $chat_deleted_count = self::mxchat_count_chat_deleted_leads();
2353 -
2354 - // Total leads = unique emails across all three sources (dedup priority: transcripts > chat_deleted > orphan
2355 - // is already enforced at collection time in mxchat_fetch_leads; stats re-apply it here).
2356 - $total_leads = $total_transcripts_emails + $chat_deleted_count + $orphan_count;
2357 -
2358 - $avg = $total_transcripts_emails > 0
2359 - ? round($total_convos / $total_transcripts_emails, 1)
2360 - : 0;
2361 -
2362 - // Orphan % reflects *true* orphans only (pre-chat dropoffs). Chat-deleted leads are
2363 - // excluded so the metric stays meaningful — admins shouldn't see their cleanups
2364 - // inflate this number.
2365 - $orphan_pct = $total_leads > 0
2366 - ? (int) round(($orphan_count / $total_leads) * 100)
2367 - : 0;
2368 -
2369 - return [
2370 - 'total_leads' => $total_leads,
2371 - 'new_this_week' => $new_this_week,
2372 - 'avg_convos' => $avg,
2373 - 'orphan_pct' => $orphan_pct,
2374 - 'orphan_count' => $orphan_count,
2375 - 'chat_deleted_count' => $chat_deleted_count,
2376 - ];
2377 -}
2378 -
2379 -/**
2380 - * Collect leads who had a conversation that an admin later deleted (preserved via
2381 - * mxchat_lead_del_* options). Returns rows tagged status='chat_deleted' with the
2382 - * original last-seen timestamp so they still sort and filter sensibly.
2383 - *
2384 - * @param string $search Optional email/name substring filter.
2385 - * @param string $date_cutoff Optional 'Y-m-d H:i:s' cutoff — only rows with last_ts >= cutoff.
2386 - * @return array
2387 - */
2388 -private static function mxchat_collect_chat_deleted_leads($search = '', $date_cutoff = '') {
2389 - global $wpdb;
2390 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2391 -
2392 - $rows = $wpdb->get_results(
2393 - "SELECT option_name, option_value FROM {$wpdb->options}
2394 - WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2395 - );
2396 - if (empty($rows)) {
2397 - return [];
2398 - }
2399 -
2400 - // Emails that currently have transcripts rows should not appear as chat_deleted —
2401 - // they've come back and chatted, so they're active leads again.
2402 - $emails_in_transcripts = array_map(
2403 - 'strtolower',
2404 - (array) $wpdb->get_col(
2405 - "SELECT DISTINCT user_email FROM {$table}
2406 - WHERE user_email IS NOT NULL AND user_email != ''"
2407 - )
2408 - );
2409 - $emails_in_transcripts = array_flip($emails_in_transcripts);
2410 -
2411 - $needle = strtolower(trim((string) $search));
2412 - $by_email = [];
2413 -
2414 - foreach ($rows as $opt) {
2415 - $email = sanitize_email(trim((string) $opt->option_value));
2416 - if (!$email) {
2417 - continue;
2418 - }
2419 - if (isset($emails_in_transcripts[strtolower($email)])) {
2420 - continue;
2421 - }
2422 - $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
2423 - if (!$sid) {
2424 - continue;
2425 - }
2426 - $name = (string) get_option('mxchat_lead_del_name_' . $sid, '');
2427 - $ts = (string) get_option('mxchat_lead_del_ts_' . $sid, '');
2428 -
2429 - if ($date_cutoff !== '' && ($ts === '' || $ts < $date_cutoff)) {
2430 - continue;
2431 - }
2432 - if ($needle !== '') {
2433 - $hay = strtolower($email . ' ' . $name);
2434 - if (strpos($hay, $needle) === false) {
2435 - continue;
2436 - }
2437 - }
2438 -
2439 - $key = strtolower($email);
2440 - if (!isset($by_email[$key]) || (isset($by_email[$key]['last_seen']) && $ts > $by_email[$key]['last_seen'])) {
2441 - $by_email[$key] = [
2442 - 'email' => $email,
2443 - 'name' => $name,
2444 - 'conversation_count' => 0,
2445 - 'last_seen' => $ts,
2446 - 'last_seen_display' => $ts ? self::mxchat_leads_format_relative($ts) : __('Chat deleted', 'mxchat'),
2447 - 'first_seen' => $ts,
2448 - 'latest_session_id' => '',
2449 - 'top_page_url' => '',
2450 - 'top_page_title' => '',
2451 - 'is_orphan' => false,
2452 - 'status' => 'chat_deleted',
2453 - ];
2454 - }
2455 - }
2456 -
2457 - // Newest chat_deleted first.
2458 - usort($by_email, function ($a, $b) {
2459 - return strcmp((string) $b['last_seen'], (string) $a['last_seen']);
2460 - });
2461 - return array_values($by_email);
2462 -}
2463 -
2464 -/**
2465 - * Count unique emails preserved as "chat deleted" (for the stats strip).
2466 - */
2467 -private static function mxchat_count_chat_deleted_leads() {
2468 - global $wpdb;
2469 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2470 -
2471 - $emails = $wpdb->get_col(
2472 - "SELECT DISTINCT option_value FROM {$wpdb->options}
2473 - WHERE option_name LIKE 'mxchat_lead_del_email_%'"
2474 - );
2475 - if (empty($emails)) {
2476 - return 0;
2477 - }
2478 -
2479 - $transcripts_emails = array_map(
2480 - 'strtolower',
2481 - (array) $wpdb->get_col(
2482 - "SELECT DISTINCT user_email FROM {$table}
2483 - WHERE user_email IS NOT NULL AND user_email != ''"
2484 - )
2485 - );
2486 - $transcripts_emails = array_flip($transcripts_emails);
2487 -
2488 - $count = 0;
2489 - $seen = [];
2490 - foreach ($emails as $raw) {
2491 - $email = strtolower(trim((string) $raw));
2492 - if (!$email || isset($seen[$email]) || isset($transcripts_emails[$email])) {
2493 - continue;
2494 - }
2495 - $seen[$email] = true;
2496 - $count++;
2497 - }
2498 - return $count;
2499 -}
2500 -
2501 -/**
2502 - * Find leads who submitted the pre-chat form but never produced a transcripts row.
2503 - * Returned rows have no conversation_count, no timestamp.
2504 - *
2505 - * @param string $search Optional email/name substring filter.
2506 - * @return array
2507 - */
2508 -private static function mxchat_collect_orphan_leads($search = '') {
2509 - global $wpdb;
2510 - $table = $wpdb->prefix . 'mxchat_chat_transcripts';
2511 -
2512 - $option_rows = $wpdb->get_results(
2513 - "SELECT option_name, option_value FROM {$wpdb->options}
2514 - WHERE option_name LIKE 'mxchat_email_%'"
2515 - );
2516 - if (empty($option_rows)) {
2517 - return [];
2518 - }
2519 -
2520 - // Collect all session_ids that have real transcripts rows so we can exclude them.
2521 - $session_ids_with_rows = $wpdb->get_col(
2522 - "SELECT DISTINCT session_id FROM {$table}
2523 - WHERE user_email IS NOT NULL AND user_email != ''"
2524 - );
2525 - $session_ids_with_rows = array_flip($session_ids_with_rows);
2526 -
2527 - // Seen emails in transcripts (so orphans only include truly never-chatted leads).
2528 - $emails_in_transcripts = array_map(
2529 - 'strtolower',
2530 - (array) $wpdb->get_col(
2531 - "SELECT DISTINCT user_email FROM {$table}
2532 - WHERE user_email IS NOT NULL AND user_email != ''"
2533 - )
2534 - );
2535 - $emails_in_transcripts = array_flip($emails_in_transcripts);
2536 -
2537 - $orphans_by_email = [];
2538 - $needle = strtolower(trim((string) $search));
2539 -
2540 - foreach ($option_rows as $opt) {
2541 - $email = sanitize_email(trim((string) $opt->option_value));
2542 - if (!$email) {
2543 - continue;
2544 - }
2545 - $sid = substr($opt->option_name, strlen('mxchat_email_'));
2546 - if (!$sid) {
2547 - continue;
2548 - }
2549 - // Exclude leads who have any transcripts rows (they appear in the main list).
2550 - if (isset($emails_in_transcripts[strtolower($email)])) {
2551 - continue;
2552 - }
2553 - if (isset($session_ids_with_rows[$sid])) {
2554 - continue;
2555 - }
2556 -
2557 - $name_option = get_option('mxchat_name_' . $sid, '');
2558 - $name = is_string($name_option) ? trim($name_option) : '';
2559 -
2560 - if ($needle !== '') {
2561 - $hay = strtolower($email . ' ' . $name);
2562 - if (strpos($hay, $needle) === false) {
2563 - continue;
2564 - }
2565 - }
2566 -
2567 - $key = strtolower($email);
2568 - if (!isset($orphans_by_email[$key])) {
2569 - $orphans_by_email[$key] = [
2570 - 'email' => $email,
2571 - 'name' => $name,
2572 - 'conversation_count' => 0,
2573 - 'last_seen' => '',
2574 - 'last_seen_display' => __('No conversation yet', 'mxchat'),
2575 - 'first_seen' => '',
2576 - 'latest_session_id' => '',
2577 - 'top_page_url' => '',
2578 - 'top_page_title' => '',
2579 - 'is_orphan' => true,
2580 - 'status' => 'orphan',
2581 - ];
2582 - }
2583 - }
2584 -
2585 - return array_values($orphans_by_email);
2586 -}
2587 -
2588 -/**
2589 - * Map a date_range key to a SQL-comparable cutoff string, or '' for all-time.
2590 - */
2591 -private static function mxchat_leads_date_cutoff($date_range) {
2592 - switch ($date_range) {
2593 - case 'today': return gmdate('Y-m-d H:i:s', strtotime('-24 hours'));
2594 - case '7d': return gmdate('Y-m-d H:i:s', strtotime('-7 days'));
2595 - case '30d': return gmdate('Y-m-d H:i:s', strtotime('-30 days'));
2596 - case '90d': return gmdate('Y-m-d H:i:s', strtotime('-90 days'));
2597 - case 'all':
2598 - default: return '';
2599 - }
2600 -}
2601 -
2602 -/**
2603 - * Turn a UTC timestamp into a short relative display like "2h ago" or "Apr 12".
2604 - */
2605 -private static function mxchat_leads_format_relative($timestamp) {
2606 - if (!$timestamp) {
2607 - return '';
2608 - }
2609 - $ts = strtotime($timestamp . ' UTC');
2610 - if (!$ts) {
2611 - return '';
2612 - }
2613 - $diff = time() - $ts;
2614 - if ($diff < 60) return __('just now', 'mxchat');
2615 - if ($diff < 3600) return floor($diff / 60) . __('m ago', 'mxchat');
2616 - if ($diff < 86400) return floor($diff / 3600) . __('h ago', 'mxchat');
2617 - if ($diff < 604800) return floor($diff / 86400) . __('d ago', 'mxchat');
2618 - return wp_date('M j', $ts);
2619 -}
2620 -
2621 -/**
2622 1509 * Handle translation of chat messages via AJAX
2623 1510 */
2624 1511 public function mxchat_translate_messages() {
2625 1512 if (!current_user_can('manage_options')) {
@@ -2902,12 +1789,8 @@
2902 1789 /**
2903 1790 * Translate text using Claude API
2904 1791 */
2905 1792 private function translate_with_claude($api_key, $model, $system_prompt, $text) {
2906 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
2907 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
2908 - if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; }
2909 - elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; }
2910 1793 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [
2911 1794 'timeout' => 60,
2912 1795 'headers' => [
2913 1796 'x-api-key' => $api_key,
@@ -3018,11 +1901,8 @@
3018 1901 /**
3019 1902 * Translate text using Google Gemini API
3020 1903 */
3021 1904 private function translate_with_gemini($api_key, $model, $system_prompt, $text) {
3022 - if ($model === 'gemini-3-pro-preview') {
3023 - $model = 'gemini-3.1-pro-preview';
3024 - }
3025 1905 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
3026 1906
3027 1907 $response = wp_remote_post($url, [
3028 1908 'timeout' => 60,
@@ -3112,20 +1992,9 @@
3112 1992 $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
3113 1993 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50;
3114 1994 $offset = ($page - 1) * $per_page;
3115 1995 $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
3116 - $sort_raw = isset($_POST['sort_order']) ? sanitize_key($_POST['sort_order']) : 'desc';
3117 - $allowed_sorts = array('asc', 'desc', 'rating_positive', 'rating_negative');
3118 - if (!in_array($sort_raw, $allowed_sorts, true)) { $sort_raw = 'desc'; }
3119 - $sort_order = ($sort_raw === 'asc') ? 'ASC' : 'DESC';
3120 - $ratings_table = $wpdb->prefix . 'mxchat_session_ratings';
3121 - $ratings_join = '';
3122 - $rating_table_exists = ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $ratings_table)) === $ratings_table);
3123 - if ($rating_table_exists && ($sort_raw === 'rating_positive' || $sort_raw === 'rating_negative')) {
3124 - // We sort by rating then recency. Wrap rating_value so NULL sorts last.
3125 - $rating_dir = ($sort_raw === 'rating_positive') ? 'DESC' : 'ASC';
3126 - $ratings_join = " LEFT JOIN {$ratings_table} r ON r.session_id = t.session_id ";
3127 - }
1996 + $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC';
3128 1997
3129 1998 // Build search condition
3130 1999 $search_condition = '';
3131 2000 $search_params = [];
@@ -3145,24 +2014,18 @@
3145 2014 ? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params)
3146 2015 : "SELECT COUNT(DISTINCT session_id) FROM {$table_name}";
3147 2016 $total_sessions = (int) $wpdb->get_var($count_query);
3148 2017
3149 - // Get session IDs for current page. Default sort is recency; rating sorts join the ratings table
3150 - // and order by rating value (NULLs last) with recency as tiebreaker.
3151 - if ($ratings_join) {
3152 - $order_by = "ORDER BY (r.rating_value IS NULL), r.rating_value {$rating_dir}, MAX(t.timestamp) DESC";
3153 - } else {
3154 - $order_by = "ORDER BY MAX(t.timestamp) {$sort_order}";
3155 - }
2018 + // Get session IDs for current page (sorted by newest or oldest)
3156 2019 $session_query = !empty($search)
3157 2020 ? $wpdb->prepare(
3158 - "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join} {$search_condition}
3159 - GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
2021 + "SELECT DISTINCT t.session_id FROM {$table_name} t {$search_condition}
2022 + GROUP BY t.session_id ORDER BY MAX(t.timestamp) {$sort_order} LIMIT %d OFFSET %d",
3160 2023 array_merge($search_params, [$per_page, $offset])
3161 2024 )
3162 2025 : $wpdb->prepare(
3163 - "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join}
3164 - GROUP BY t.session_id {$order_by} LIMIT %d OFFSET %d",
2026 + "SELECT DISTINCT session_id FROM {$table_name}
2027 + GROUP BY session_id ORDER BY MAX(timestamp) {$sort_order} LIMIT %d OFFSET %d",
3165 2028 $per_page, $offset
3166 2029 );
3167 2030 $session_ids = $wpdb->get_col($session_query);
3168 2031
@@ -3184,24 +2047,8 @@
3184 2047 // Check for optional columns/tables
3185 2048 $url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table;
3186 2049 $originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'"));
3187 2050
3188 - // Batch-fetch session ratings for this page (plan-a5b006).
3189 - $ratings_map = array();
3190 - if ($rating_table_exists && !empty($session_ids)) {
3191 - $placeholders = implode(',', array_fill(0, count($session_ids), '%s'));
3192 - $rating_rows = $wpdb->get_results($wpdb->prepare(
3193 - "SELECT session_id, rating_value, rating_feedback FROM {$ratings_table} WHERE session_id IN ($placeholders)",
3194 - $session_ids
3195 - ));
3196 - foreach ($rating_rows as $row) {
3197 - $ratings_map[$row->session_id] = array(
3198 - 'value' => (int) $row->rating_value,
3199 - 'feedback' => (string) $row->rating_feedback,
3200 - );
3201 - }
3202 - }
3203 -
3204 2051 // Build session list data
3205 2052 $sessions = [];
3206 2053 foreach ($session_ids as $session_id) {
3207 2054 // Get session metadata
@@ -3258,12 +2105,8 @@
3258 2105 $parts = explode(' ', $display_name);
3259 2106 $initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
3260 2107 }
3261 2108
3262 - $rating_entry = isset($ratings_map[$session_id]) ? $ratings_map[$session_id] : null;
3263 - $rating_value = $rating_entry ? $rating_entry['value'] : null;
3264 - $rating_feedback = $rating_entry ? $rating_entry['feedback'] : '';
3265 -
3266 2109 $sessions[] = [
3267 2110 'session_id' => $session_id,
3268 2111 'display_name' => $display_name,
3269 2112 'display_sub' => $display_sub,
@@ -3270,11 +2113,9 @@
3270 2113 'initials' => $initials,
3271 2114 'preview' => $preview,
3272 2115 'message_count' => (int) $message_stats->count,
3273 2116 'time_display' => $time_display,
3274 - 'timestamp' => $message_stats->latest,
3275 - 'rating_value' => $rating_value,
3276 - 'rating_feedback' => $rating_feedback,
2117 + 'timestamp' => $message_stats->latest
3277 2118 ];
3278 2119 }
3279 2120
3280 2121 wp_send_json([
@@ -3412,18 +2253,8 @@
3412 2253
3413 2254 // First message timestamp for "started" display
3414 2255 $started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-';
3415 2256
3416 - // Pull rating_feedback for this session (if any) so the details drawer can show it.
3417 - $rating_feedback = '';
3418 - $ratings_table_det = $wpdb->prefix . 'mxchat_session_ratings';
3419 - if ($wpdb->get_var("SHOW TABLES LIKE '$ratings_table_det'") === $ratings_table_det) {
3420 - $rating_feedback = (string) $wpdb->get_var($wpdb->prepare(
3421 - "SELECT rating_feedback FROM {$ratings_table_det} WHERE session_id = %s LIMIT 1",
3422 - $session_id
3423 - ));
3424 - }
3425 -
3426 2257 wp_send_json([
3427 2258 'success' => true,
3428 2259 'session_id' => $session_id,
3429 2260 'user' => [
@@ -3439,10 +2270,9 @@
3439 2270 ],
3440 2271 'clicked_urls' => $clicked_urls,
3441 2272 'messages' => $formatted_messages,
3442 2273 'message_count' => count($messages),
3443 - 'started' => $started,
3444 - 'rating_feedback' => $rating_feedback
2274 + 'started' => $started
3445 2275 ]);
3446 2276 wp_die();
3447 2277 }
3448 2278
@@ -3895,103 +2725,53 @@
3895 2725 echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]);
3896 2726 wp_die();
3897 2727 }
3898 2728 check_ajax_referer('mxchat_delete_chat_history', 'security');
3899 -
3900 - if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) {
3901 - echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
3902 - wp_die();
3903 - }
3904 -
3905 2729 global $wpdb;
3906 2730 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3907 - $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
3908 - $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table;
3909 2731
3910 - // When true, any lead attached to these sessions is fully wiped (all their sessions,
3911 - // across the whole table). Default false: the chat rows go away but the lead is
3912 - // preserved as a separate "chat deleted" lead in the Leads tab.
3913 - $also_delete_lead = !empty($_POST['also_delete_lead']) && $_POST['also_delete_lead'] !== 'false';
2732 + if (isset($_POST['delete_session_ids']) && is_array($_POST['delete_session_ids'])) {
2733 + $deleted_count = 0;
2734 + $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
3914 2735
3915 - $deleted_count = 0;
3916 - $preserved_as_deleted_leads = 0;
3917 - $emails_to_fully_wipe = [];
2736 + foreach ($_POST['delete_session_ids'] as $session_id) {
2737 + $session_id_sanitized = sanitize_text_field($session_id);
3918 2738
3919 - foreach ((array) $_POST['delete_session_ids'] as $session_id) {
3920 - $session_id_sanitized = sanitize_text_field($session_id);
3921 - if ($session_id_sanitized === '') {
3922 - continue;
3923 - }
2739 + // Clear relevant cache before deletion
2740 + $cache_key = 'chat_session_' . $session_id_sanitized;
2741 + wp_cache_delete($cache_key, 'mxchat_chat_sessions');
3924 2742
3925 - // Capture the lead info attached to this session *before* we delete the rows.
3926 - $lead_row = $wpdb->get_row($wpdb->prepare(
3927 - "SELECT user_email, user_name, MAX(timestamp) AS last_ts
3928 - FROM {$table_name}
3929 - WHERE session_id = %s AND user_email IS NOT NULL AND user_email != ''
3930 - GROUP BY user_email, user_name
3931 - ORDER BY last_ts DESC LIMIT 1",
3932 - $session_id_sanitized
3933 - ));
2743 + // Perform the deletion from the database table
2744 + $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
3934 2745
3935 - wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions');
3936 - $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]);
2746 + // Delete any saved translations for this session
2747 + if ($wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) {
2748 + $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
2749 + }
3937 2750
3938 - if ($has_translations) {
3939 - $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]);
3940 - }
2751 + // Delete the corresponding option entry from wp_options table
2752 + delete_option("mxchat_history_" . $session_id_sanitized);
3941 2753
3942 - delete_option('mxchat_history_' . $session_id_sanitized);
3943 - delete_option('mxchat_agent_name_' . $session_id_sanitized);
2754 + // Delete any associated metadata options
2755 + delete_option("mxchat_email_" . $session_id_sanitized);
2756 + delete_option("mxchat_agent_name_" . $session_id_sanitized);
3944 2757
3945 - if ($lead_row && !empty($lead_row->user_email)) {
3946 - if ($also_delete_lead) {
3947 - // Full-wipe requested — queue the email so that all their sessions and
3948 - // related options get swept below. Also clear this session's pre-chat
3949 - // capture options (they're no longer meaningful).
3950 - $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email;
3951 - delete_option('mxchat_email_' . $session_id_sanitized);
3952 - delete_option('mxchat_name_' . $session_id_sanitized);
3953 - } else {
3954 - // Preserve the lead in a "Chat deleted" state via distinct option keys so
3955 - // they stay out of the orphan bucket (orphan = pre-chat form dropoff).
3956 - update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false);
3957 - if (!empty($lead_row->user_name)) {
3958 - update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false);
3959 - }
3960 - if (!empty($lead_row->last_ts)) {
3961 - update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false);
3962 - }
3963 - // Clean up pre-chat capture options for this session — chat_deleted supersedes.
3964 - delete_option('mxchat_email_' . $session_id_sanitized);
3965 - delete_option('mxchat_name_' . $session_id_sanitized);
3966 - $preserved_as_deleted_leads++;
3967 - }
3968 - } else {
3969 - // No lead attached — nothing to preserve. Clean up any orphan options anyway.
3970 - delete_option('mxchat_email_' . $session_id_sanitized);
3971 - delete_option('mxchat_name_' . $session_id_sanitized);
2758 + $deleted_count++;
3972 2759 }
3973 2760
3974 - $deleted_count++;
3975 - }
2761 + // Optionally, clear a general cache if you have one
2762 + wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
3976 2763
3977 - // Opt-in full-lead wipe: sweep every remaining row + every option key (including
3978 - // chat_deleted preservation) for each affected email. Reuses the same internal
3979 - // helper as the Leads-tab Delete button for consistency.
3980 - if (!empty($emails_to_fully_wipe)) {
3981 - self::mxchat_wipe_leads_by_email(array_values($emails_to_fully_wipe));
2764 + echo wp_json_encode([
2765 + 'success' => sprintf(
2766 + esc_html__('%d chat session(s) have been deleted from all storage locations.', 'mxchat'),
2767 + $deleted_count
2768 + )
2769 + ]);
2770 + } else {
2771 + echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]);
3982 2772 }
3983 2773
3984 - wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions');
3985 -
3986 - echo wp_json_encode([
3987 - 'success' => sprintf(
3988 - esc_html__('%d chat session(s) have been deleted.', 'mxchat'),
3989 - $deleted_count
3990 - ),
3991 - 'preserved_as_deleted_leads' => $preserved_as_deleted_leads,
3992 - 'leads_fully_wiped' => count($emails_to_fully_wipe),
3993 - ]);
3994 2774 wp_die();
3995 2775 }
3996 2776
3997 2777 /**
@@ -4247,58 +3027,8 @@
4247 3027 : $row->callback_function;
4248 3028 $action_type_distribution[$label] = $row->count;
4249 3029 }
4250 3030
4251 - // Native function-calling (AI Tools) data — plan-mxchat-20260617-a41dee.
4252 - // The AI Tools checklist reads from MxChat_Tool_Registry, the SAME single
4253 - // source the chat-time function-calling loop reads, so the two never drift.
4254 - if (!class_exists('MxChat_Tool_Registry')) {
4255 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-tool-registry.php';
4256 - }
4257 - if (!class_exists('MxChat_Model_Catalog')) {
4258 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
4259 - }
4260 - $fc_options = get_option('mxchat_options', array());
4261 - $fc_current_model = isset($fc_options['model']) ? $fc_options['model'] : 'gpt-5.1-chat-latest';
4262 - $fc_model_capable = class_exists('MxChat_Model_Catalog')
4263 - ? MxChat_Model_Catalog::supports_tools($fc_current_model) : true;
4264 - $active_tab = (isset($_GET['tab']) && $_GET['tab'] === 'ai-tools') ? 'ai-tools' : 'dashboard';
4265 -
4266 - // Enrich each AI Tool with the dashicon its callback already uses on the
4267 - // Trigger Phrases "Choose what it does" grid, so the AI Tools cards/modal
4268 - // share the same iconography (plan 8bbf98 part 4). View-layer only — the
4269 - // registry's model-facing data is untouched. Default admin-generic.
4270 - $fc_tools = MxChat_Tool_Registry::available_tools();
4271 - foreach ($fc_tools as &$fc_tool_ref) {
4272 - $fc_cb_ref = $fc_tool_ref['callback'];
4273 - $fc_tool_ref['icon'] = isset($available_callbacks[$fc_cb_ref]['icon'])
4274 - ? $available_callbacks[$fc_cb_ref]['icon']
4275 - : 'admin-generic';
4276 - }
4277 - unset($fc_tool_ref);
4278 -
4279 - // Count of ACTIVE tools — drives the AI Tools sidebar nav badge, mirroring
4280 - // $total_actions for Trigger Phrases. A tool in the list = active (plan
4281 - // d450a7), so the badge shows the same number the list pane shows (plan 5f7409).
4282 - $total_tools = count(array_filter($fc_tools, function ($t) {
4283 - return !empty($t['enabled']);
4284 - }));
4285 -
4286 - // Brave-key dependency check (plan 183856): Web Search + Image Search run on
4287 - // the Brave Search API. If either is enabled as a tool but no brave_api_key is
4288 - // configured, surface a graceful "key not set" notice so the admin isn't met
4289 - // with a silently no-firing tool.
4290 - $fc_brave_missing = false;
4291 - $brave_key = isset($fc_options['brave_api_key']) ? trim($fc_options['brave_api_key']) : '';
4292 - if ($brave_key === '') {
4293 - foreach ($fc_tools as $fc_t) {
4294 - if (!empty($fc_t['enabled']) && isset($fc_t['requires_key']) && $fc_t['requires_key'] === 'brave_api_key') {
4295 - $fc_brave_missing = true;
4296 - break;
4297 - }
4298 - }
4299 - }
4300 -
4301 3031 // Prepare page data
4302 3032 $page_data = array(
4303 3033 'total_actions' => $total_actions,
4304 3034 'enabled_actions' => $enabled_actions,
@@ -4306,17 +3036,8 @@
4306 3036 'action_types_count' => $action_types_count,
4307 3037 'action_type_distribution' => $action_type_distribution,
4308 3038 'available_callbacks' => $available_callbacks,
4309 3039 'callback_groups' => $callback_groups,
4310 - // AI Tools section
4311 - 'active_tab' => $active_tab,
4312 - 'fc_enabled' => MxChat_Tool_Registry::is_enabled(),
4313 - 'total_tools' => $total_tools,
4314 - 'fc_tools' => $fc_tools,
4315 - 'fc_current_model' => $fc_current_model,
4316 - 'fc_model_capable' => $fc_model_capable,
4317 - 'fc_brave_missing' => $fc_brave_missing,
4318 - 'fc_saved' => isset($_GET['mxchat_fc_saved']),
4319 3040 );
4320 3041
4321 3042 // Include and render the new template
4322 3043 require_once plugin_dir_path(__FILE__) . 'admin-actions-page.php';
@@ -4926,9 +3647,9 @@
4926 3647 'intent_label' => $intent_label,
4927 3648 'phrases' => implode(', ', $phrases_array),
4928 3649 'embedding_vector' => $serialized_vector,
4929 3650 'similarity_threshold' => $similarity_threshold,
4930 - 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update
3651 + 'enabled_bots' => $enabled_bots_json // Include enabled_bots in update
4931 3652 ),
4932 3653 array('id' => $intent_id),
4933 3654 array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string
4934 3655 array('%d') // Where format: integer
@@ -5045,9 +3766,9 @@
5045 3766 'phrases' => implode(', ', $phrases_array),
5046 3767 'embedding_vector' => $serialized_vector,
5047 3768 'callback_function' => $callback_function,
5048 3769 'similarity_threshold' => $similarity_threshold,
5049 - 'enabled_bots' => $enabled_bots_json, // NEW field
3770 + 'enabled_bots' => $enabled_bots_json // NEW field
5050 3771 ]);
5051 3772
5052 3773 if ($result === false) {
5053 3774 $this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error);
@@ -5130,25 +3851,8 @@
5130 3851
5131 3852 // Get available callbacks for labels/icons
5132 3853 $available_callbacks = $this->mxchat_get_available_callbacks();
5133 3854
5134 - // Prefetch individual phrase counts for all fetched actions
5135 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5136 - $phrase_counts = array();
5137 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5138 - $action_ids = wp_list_pluck($actions, 'id');
5139 - if (!empty($action_ids)) {
5140 - $id_placeholders = implode(',', array_fill(0, count($action_ids), '%d'));
5141 - $count_results = $wpdb->get_results($wpdb->prepare(
5142 - "SELECT intent_id, COUNT(*) as cnt FROM $phrases_table WHERE intent_id IN ($id_placeholders) GROUP BY intent_id",
5143 - $action_ids
5144 - ));
5145 - foreach ($count_results as $row) {
5146 - $phrase_counts[$row->intent_id] = intval($row->cnt);
5147 - }
5148 - }
5149 - }
5150 -
5151 3855 // Format actions for response
5152 3856 $formatted_actions = array();
5153 3857 foreach ($actions as $action) {
5154 3858 $callback_data = isset($available_callbacks[$action->callback_function])
@@ -5169,10 +3873,8 @@
5169 3873 'icon' => isset($callback_data['icon']) ? $callback_data['icon'] : 'admin-generic',
5170 3874 'threshold' => round($action->similarity_threshold * 100),
5171 3875 'enabled' => (bool) $action->enabled,
5172 3876 'enabled_bots' => $enabled_bots,
5173 - 'has_legacy_vector' => !empty($action->embedding_vector),
5174 - 'individual_phrase_count' => isset($phrase_counts[$action->id]) ? $phrase_counts[$action->id] : 0,
5175 3877 );
5176 3878 }
5177 3879
5178 3880 $total_pages = ceil($total_actions / $per_page);
@@ -5251,14 +3953,8 @@
5251 3953 if ($result === false) {
5252 3954 wp_send_json_error(__('Failed to delete actions', 'mxchat'));
5253 3955 }
5254 3956
5255 - // Also delete individual phrases for these intents
5256 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5257 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5258 - $wpdb->query($wpdb->prepare("DELETE FROM $phrases_table WHERE intent_id IN ($placeholders)", $action_ids));
5259 - }
5260 -
5261 3957 wp_send_json_success(array('deleted' => $result));
5262 3958 }
5263 3959
5264 3960 /**
@@ -5281,18 +3977,25 @@
5281 3977 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5282 3978 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5283 3979
5284 3980 // Validate
5285 - // Check if using new individual phrases mode or legacy mode
5286 - $individual_phrases = isset($_POST['individual_phrases']) ? array_filter(array_map('sanitize_text_field', (array) $_POST['individual_phrases'])) : array();
5287 - $use_individual = !empty($individual_phrases);
3981 + if (empty($intent_label) || empty($phrases_input) || empty($callback_function)) {
3982 + wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
3983 + }
5288 3984
5289 - // Validate required fields (phrases not required when using individual mode)
5290 - if (empty($intent_label) || empty($callback_function)) {
5291 - wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
3985 + // Process phrases
3986 + $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
3987 + if (empty($phrases_array)) {
3988 + wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5292 3989 }
5293 - if (!$use_individual && empty($phrases_input)) {
5294 - wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
3990 +
3991 + // Generate embedding (combine phrases into single string for embedding)
3992 + $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
3993 + if (is_wp_error($embedding_vector)) {
3994 + // Fallback: store without embedding
3995 + $serialized_vector = null;
3996 + } else {
3997 + $serialized_vector = maybe_serialize($embedding_vector);
5295 3998 }
5296 3999
5297 4000 // Ensure default bot is included
5298 4001 if (!in_array('default', $enabled_bots)) {
@@ -5299,95 +4002,27 @@
5299 4002 $enabled_bots[] = 'default';
5300 4003 }
5301 4004 $enabled_bots_json = json_encode($enabled_bots);
5302 4005
5303 - if ($use_individual) {
5304 - // New mode: individual phrases each get their own vector
5305 - // Insert the intent row with empty legacy fields
5306 - $result = $wpdb->insert(
5307 - $table_name,
5308 - array(
5309 - 'intent_label' => $intent_label,
5310 - 'phrases' => '',
5311 - 'embedding_vector' => '',
5312 - 'similarity_threshold' => $similarity_threshold,
5313 - 'callback_function' => $callback_function,
5314 - 'enabled' => 1,
5315 - 'enabled_bots' => $enabled_bots_json,
5316 - )
5317 - );
4006 + // Insert
4007 + $result = $wpdb->insert(
4008 + $table_name,
4009 + array(
4010 + 'intent_label' => $intent_label,
4011 + 'phrases' => implode(', ', $phrases_array),
4012 + 'embedding_vector' => $serialized_vector,
4013 + 'similarity_threshold' => $similarity_threshold,
4014 + 'callback_function' => $callback_function,
4015 + 'enabled' => 1,
4016 + 'enabled_bots' => $enabled_bots_json,
4017 + )
4018 + );
5318 4019
5319 - if ($result === false) {
5320 - wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5321 - }
4020 + if ($result === false) {
4021 + wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
4022 + }
5322 4023
5323 - $intent_id = $wpdb->insert_id;
5324 -
5325 - // Insert each phrase individually with its own embedding
5326 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5327 - $failed_phrases = array();
5328 - foreach ($individual_phrases as $phrase) {
5329 - $phrase = trim($phrase);
5330 - if (empty($phrase)) continue;
5331 -
5332 - $embedding_vector = $this->mxchat_generate_embedding($phrase);
5333 - if (is_wp_error($embedding_vector)) {
5334 - $failed_phrases[] = $phrase;
5335 - continue;
5336 - }
5337 -
5338 - $wpdb->insert(
5339 - $phrases_table,
5340 - array(
5341 - 'intent_id' => $intent_id,
5342 - 'phrase' => $phrase,
5343 - 'embedding_vector' => maybe_serialize($embedding_vector),
5344 - )
5345 - );
5346 - }
5347 -
5348 - $response = array('id' => $intent_id);
5349 - if (!empty($failed_phrases)) {
5350 - $response['failed_phrases'] = $failed_phrases;
5351 - }
5352 - wp_send_json_success($response);
5353 -
5354 - } else {
5355 - // Legacy mode: combine all phrases into one embedding (backwards compatible)
5356 - $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5357 - if (empty($phrases_array)) {
5358 - wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5359 - }
5360 -
5361 - // Generate embedding (combine phrases into single string for embedding)
5362 - $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5363 - if (is_wp_error($embedding_vector)) {
5364 - // Fallback: store without embedding
5365 - $serialized_vector = null;
5366 - } else {
5367 - $serialized_vector = maybe_serialize($embedding_vector);
5368 - }
5369 -
5370 - // Insert
5371 - $result = $wpdb->insert(
5372 - $table_name,
5373 - array(
5374 - 'intent_label' => $intent_label,
5375 - 'phrases' => implode(', ', $phrases_array),
5376 - 'embedding_vector' => $serialized_vector,
5377 - 'similarity_threshold' => $similarity_threshold,
5378 - 'callback_function' => $callback_function,
5379 - 'enabled' => 1,
5380 - 'enabled_bots' => $enabled_bots_json,
5381 - )
5382 - );
5383 -
5384 - if ($result === false) {
5385 - wp_send_json_error(__('Failed to add action to database.', 'mxchat'));
5386 - }
5387 -
5388 - wp_send_json_success(array('id' => $wpdb->insert_id));
5389 - }
4024 + wp_send_json_success(array('id' => $wpdb->insert_id));
5390 4025 }
5391 4026
5392 4027 /**
5393 4028 * AJAX handler to edit an existing intent/action
@@ -5410,52 +4045,40 @@
5410 4045 $similarity_threshold = isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) / 100 : 0.85;
5411 4046 $enabled_bots = isset($_POST['enabled_bots']) ? array_map('sanitize_text_field', (array) $_POST['enabled_bots']) : array('default');
5412 4047
5413 4048 // Validate
5414 - if (!$intent_id || empty($intent_label)) {
4049 + if (!$intent_id || empty($intent_label) || empty($phrases_input)) {
5415 4050 wp_send_json_error(__('Please fill in all required fields.', 'mxchat'));
5416 4051 }
5417 4052
5418 - // Check if phrases are managed individually (empty phrases_input means individual mode)
5419 - $uses_individual_phrases = empty($phrases_input);
4053 + // Process phrases
4054 + $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
4055 + if (empty($phrases_array)) {
4056 + wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
4057 + }
5420 4058
5421 - if ($uses_individual_phrases) {
5422 - // Individual phrase mode: only update non-phrase fields, skip embedding regeneration
4059 + // Generate new embedding (combine phrases into single string for embedding)
4060 + $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
4061 + if (is_wp_error($embedding_vector)) {
4062 + // Keep existing embedding
4063 + $serialized_vector = null;
5423 4064 $update_data = array(
5424 4065 'intent_label' => $intent_label,
4066 + 'phrases' => implode(', ', $phrases_array),
5425 4067 'similarity_threshold' => $similarity_threshold,
5426 4068 'callback_function' => $callback_function,
5427 4069 'enabled_bots' => json_encode($enabled_bots),
5428 4070 );
5429 4071 } else {
5430 - // Legacy mode: process phrases and regenerate embedding (backwards compatible for add-ons)
5431 - $phrases_array = array_filter(array_map('trim', preg_split('/[\n,]+/', $phrases_input)));
5432 - if (empty($phrases_array)) {
5433 - wp_send_json_error(__('Please provide at least one trigger phrase.', 'mxchat'));
5434 - }
5435 -
5436 - // Generate new embedding (combine phrases into single string for embedding)
5437 - $embedding_vector = $this->mxchat_generate_embedding(implode(' ', $phrases_array));
5438 - if (is_wp_error($embedding_vector)) {
5439 - // Keep existing embedding
5440 - $update_data = array(
5441 - 'intent_label' => $intent_label,
5442 - 'phrases' => implode(', ', $phrases_array),
5443 - 'similarity_threshold' => $similarity_threshold,
5444 - 'callback_function' => $callback_function,
5445 - 'enabled_bots' => json_encode($enabled_bots),
5446 - );
5447 - } else {
5448 - $serialized_vector = maybe_serialize($embedding_vector);
5449 - $update_data = array(
5450 - 'intent_label' => $intent_label,
5451 - 'phrases' => implode(', ', $phrases_array),
5452 - 'embedding_vector' => $serialized_vector,
5453 - 'similarity_threshold' => $similarity_threshold,
5454 - 'callback_function' => $callback_function,
5455 - 'enabled_bots' => json_encode($enabled_bots),
5456 - );
5457 - }
4072 + $serialized_vector = maybe_serialize($embedding_vector);
4073 + $update_data = array(
4074 + 'intent_label' => $intent_label,
4075 + 'phrases' => implode(', ', $phrases_array),
4076 + 'embedding_vector' => $serialized_vector,
4077 + 'similarity_threshold' => $similarity_threshold,
4078 + 'callback_function' => $callback_function,
4079 + 'enabled_bots' => json_encode($enabled_bots),
4080 + );
5458 4081 }
5459 4082
5460 4083 // Ensure default bot is included
5461 4084 if (!in_array('default', $enabled_bots)) {
@@ -5502,154 +4125,13 @@
5502 4125 if ($result === false) {
5503 4126 wp_send_json_error(__('Failed to delete action', 'mxchat'));
5504 4127 }
5505 4128
5506 - // Also delete individual phrases for this intent
5507 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5508 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5509 - $wpdb->delete($phrases_table, array('intent_id' => $intent_id), array('%d'));
5510 - }
5511 -
5512 4129 wp_send_json_success(array('deleted' => true));
5513 4130 }
5514 4131
5515 -/**
5516 - * AJAX handler to add a single phrase with its own embedding to an intent
5517 - */
5518 -public function mxchat_add_phrase_ajax() {
5519 - check_ajax_referer('mxchat_add_phrase_nonce', 'security');
5520 4132
5521 - if (!current_user_can('manage_options')) {
5522 - wp_send_json_error(__('Unauthorized', 'mxchat'));
5523 - }
5524 -
5525 - $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5526 - $phrase = isset($_POST['phrase']) ? sanitize_text_field($_POST['phrase']) : '';
5527 -
5528 - if (!$intent_id || empty($phrase)) {
5529 - wp_send_json_error(__('Please provide an intent ID and phrase.', 'mxchat'));
5530 - }
5531 -
5532 - // Verify the intent exists
5533 - global $wpdb;
5534 - $intents_table = $wpdb->prefix . 'mxchat_intents';
5535 - $intent = $wpdb->get_row($wpdb->prepare("SELECT id FROM $intents_table WHERE id = %d", $intent_id));
5536 - if (!$intent) {
5537 - wp_send_json_error(__('Action not found.', 'mxchat'));
5538 - }
5539 -
5540 - // Generate embedding for this single phrase
5541 - $embedding_vector = $this->mxchat_generate_embedding($phrase);
5542 - if (is_wp_error($embedding_vector)) {
5543 - wp_send_json_error(__('Failed to generate embedding: ', 'mxchat') . $embedding_vector->get_error_message());
5544 - }
5545 -
5546 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5547 - $result = $wpdb->insert(
5548 - $phrases_table,
5549 - array(
5550 - 'intent_id' => $intent_id,
5551 - 'phrase' => $phrase,
5552 - 'embedding_vector' => maybe_serialize($embedding_vector),
5553 - )
5554 - );
5555 -
5556 - if ($result === false) {
5557 - wp_send_json_error(__('Failed to add phrase.', 'mxchat'));
5558 - }
5559 -
5560 - wp_send_json_success(array('id' => $wpdb->insert_id, 'phrase' => $phrase));
5561 -}
5562 -
5563 4133 /**
5564 - * AJAX handler to delete a single phrase from wp_mxchat_intent_phrases
5565 - */
5566 -public function mxchat_delete_phrase_ajax() {
5567 - check_ajax_referer('mxchat_delete_phrase_nonce', 'security');
5568 -
5569 - if (!current_user_can('manage_options')) {
5570 - wp_send_json_error(__('Unauthorized', 'mxchat'));
5571 - }
5572 -
5573 - $phrase_id = isset($_POST['phrase_id']) ? intval($_POST['phrase_id']) : 0;
5574 - if (!$phrase_id) {
5575 - wp_send_json_error(__('Invalid phrase ID.', 'mxchat'));
5576 - }
5577 -
5578 - global $wpdb;
5579 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5580 - $result = $wpdb->delete($phrases_table, array('id' => $phrase_id), array('%d'));
5581 -
5582 - if ($result === false) {
5583 - wp_send_json_error(__('Failed to delete phrase.', 'mxchat'));
5584 - }
5585 -
5586 - wp_send_json_success(array('deleted' => true));
5587 -}
5588 -
5589 -/**
5590 - * AJAX handler to fetch individual phrases for an intent
5591 - */
5592 -public function mxchat_get_phrases_ajax() {
5593 - check_ajax_referer('mxchat_get_phrases_nonce', 'security');
5594 -
5595 - if (!current_user_can('manage_options')) {
5596 - wp_send_json_error(__('Unauthorized', 'mxchat'));
5597 - }
5598 -
5599 - $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5600 - if (!$intent_id) {
5601 - wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5602 - }
5603 -
5604 - global $wpdb;
5605 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
5606 -
5607 - $phrases = array();
5608 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
5609 - $phrases = $wpdb->get_results($wpdb->prepare(
5610 - "SELECT id, phrase, created_at FROM $phrases_table WHERE intent_id = %d ORDER BY created_at ASC",
5611 - $intent_id
5612 - ));
5613 - }
5614 -
5615 - wp_send_json_success(array('phrases' => $phrases));
5616 -}
5617 -
5618 -/**
5619 - * AJAX handler to clear legacy phrases and embedding from the main intents table
5620 - */
5621 -public function mxchat_delete_legacy_phrases_ajax() {
5622 - check_ajax_referer('mxchat_delete_legacy_nonce', 'security');
5623 -
5624 - if (!current_user_can('manage_options')) {
5625 - wp_send_json_error(__('Unauthorized', 'mxchat'));
5626 - }
5627 -
5628 - $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
5629 - if (!$intent_id) {
5630 - wp_send_json_error(__('Invalid intent ID.', 'mxchat'));
5631 - }
5632 -
5633 - global $wpdb;
5634 - $table_name = $wpdb->prefix . 'mxchat_intents';
5635 -
5636 - $result = $wpdb->update(
5637 - $table_name,
5638 - array('phrases' => '', 'embedding_vector' => ''),
5639 - array('id' => $intent_id),
5640 - array('%s', '%s'),
5641 - array('%d')
5642 - );
5643 -
5644 - if ($result === false) {
5645 - wp_send_json_error(__('Failed to clear legacy phrases.', 'mxchat'));
5646 - }
5647 -
5648 - wp_send_json_success(array('cleared' => true));
5649 -}
5650 -
5651 -/**
5652 4134 * Enhanced get_available_callbacks function with form action exclusion
5653 4135 *
5654 4136 * @param bool $grouped Whether to return callbacks grouped by category
5655 4137 * @param bool $include_all Whether to include all potential actions (even if add-on not installed)
@@ -5687,10 +4169,9 @@
5687 4169 'mxchat_handle_product_recommendations',
5688 4170 'mxchat_handle_order_history',
5689 4171 'mxchat_show_product_card',
5690 4172 'mxchat_add_to_cart',
5691 - 'mxchat_checkout_redirect',
5692 - 'mxchat_handle_featured_products'
4173 + 'mxchat_checkout_redirect'
5693 4174 ),
5694 4175 'name' => __('WooCommerce Add-on', 'mxchat'),
5695 4176 'pro_required' => true
5696 4177 ),
@@ -5753,9 +4234,9 @@
5753 4234 'label' => __('Brave Web Search', 'mxchat'),
5754 4235 'pro_only' => false,
5755 4236 'group' => __('Search Features', 'mxchat'),
5756 4237 'icon' => 'search',
5757 - 'description' => __('Let users search the web directly from the chat (requires a Brave Search API key)', 'mxchat'),
4238 + 'description' => __('Let users search the web directly from the chat', 'mxchat'),
5758 4239 'addon' => false,
5759 4240 'installed' => true
5760 4241 ),
5761 4242 'mxchat_handle_image_search_request' => array(
@@ -5762,9 +4243,9 @@
5762 4243 'label' => __('Brave Image Search', 'mxchat'),
5763 4244 'pro_only' => false,
5764 4245 'group' => __('Search Features', 'mxchat'),
5765 4246 'icon' => 'format-image',
5766 - 'description' => __('Search and display images in the chat conversation (requires a Brave Search API key)', 'mxchat'),
4247 + 'description' => __('Search and display images in the chat conversation', 'mxchat'),
5767 4248 'addon' => false,
5768 4249 'installed' => true
5769 4250 ),
5770 4251 // Pro core features - check is_activated property
@@ -5867,16 +4348,8 @@
5867 4348 'group' => __('WooCommerce Features', 'mxchat'),
5868 4349 'icon' => 'arrow-right-alt',
5869 4350 'description' => __('Redirect customer to checkout page', 'mxchat'),
5870 4351 ),
5871 - 'mxchat_handle_featured_products' => array(
5872 - 'label' => __('Featured Products Showcase', 'mxchat'),
5873 - 'pro_only' => false,
5874 - 'addon_promo' => true,
5875 - 'group' => __('WooCommerce Features', 'mxchat'),
5876 - 'icon' => 'star-filled',
5877 - 'description' => __('Display a curated selection of products with an AI-generated message', 'mxchat'),
5878 - ),
5879 4352
5880 4353 // Perplexity Add-on
5881 4354 'mxchat_perplexity_research' => array(
5882 4355 'label' => __('Perplexity Research', 'mxchat'),
@@ -6119,17 +4592,8 @@
6119 4592 'mxchat-api-keys',
6120 4593 'mxchat_api_keys_section'
6121 4594 );
6122 4595
6123 - // Custom (OpenAI-compatible) Provider — for Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
6124 - add_settings_field(
6125 - 'custom_provider',
6126 - esc_html__('Custom Provider (OpenAI-compatible)', 'mxchat'),
6127 - array($this, 'custom_provider_callback'),
6128 - 'mxchat-api-keys',
6129 - 'mxchat_api_keys_section'
6130 - );
6131 -
6132 4596 // Loops API Key
6133 4597 add_settings_field(
6134 4598 'loops_api_key',
6135 4599 esc_html__('Loops API Key', 'mxchat'),
@@ -6146,8 +4610,17 @@
6146 4610 'mxchat-api-keys',
6147 4611 'mxchat_api_keys_section'
6148 4612 );
6149 4613
4614 + // Frontend Debugger Toggle
4615 + add_settings_field(
4616 + 'show_frontend_debugger',
4617 + esc_html__('Frontend Debugger', 'mxchat'),
4618 + array($this, 'mxchat_frontend_debugger_callback'),
4619 + 'mxchat-chatbot',
4620 + 'mxchat_chatbot_section'
4621 + );
4622 +
6150 4623 // Similarity Threshold Slider
6151 4624 add_settings_field(
6152 4625 'similarity_threshold', // Field ID
6153 4626 esc_html__('Similarity Threshold', 'mxchat'), // Field title
@@ -6164,18 +4637,9 @@
6164 4637 'mxchat-chatbot', // Page
6165 4638 'mxchat_chatbot_section' // Section
6166 4639 );
6167 4640
6168 - // RAG Chunks Limit Slider
6169 4641 add_settings_field(
6170 - 'rag_chunks_limit', // Field ID
6171 - esc_html__('RAG Chunks Limit', 'mxchat'), // Field title
6172 - array($this, 'mxchat_rag_chunks_limit_callback'), // Callback function
6173 - 'mxchat-chatbot', // Page
6174 - 'mxchat_chatbot_section' // Section
6175 - );
6176 -
6177 - add_settings_field(
6178 4642 'append_to_body',
6179 4643 esc_html__('Auto-Display Chatbot', 'mxchat'),
6180 4644 array($this, 'mxchat_append_to_body_callback'),
6181 4645 'mxchat-chatbot',
@@ -6197,22 +4661,9 @@
6197 4661 'mxchat-chatbot',
6198 4662 'mxchat_chatbot_section'
6199 4663 );
6200 4664
6201 - // Satisfaction rating toggle (plan-a5b006).
6202 4665 add_settings_field(
6203 - 'satisfaction_rating_enabled',
6204 - esc_html__('Satisfaction Rating Prompt', 'mxchat'),
6205 - array($this, 'mxchat_satisfaction_rating_toggle_callback'),
6206 - 'mxchat-chatbot',
6207 - 'mxchat_chatbot_section'
6208 - );
6209 -
6210 - // Satisfaction rating customization (plan-141a12, plan-29caac):
6211 - // the 5 customization fields are now inline-rendered inside
6212 - // mxchat_satisfaction_rating_toggle_callback's sub-options wrapper.
6213 -
6214 - add_settings_field(
6215 4666 'enable_streaming_toggle',
6216 4667 esc_html__('Enable Streaming', 'mxchat'),
6217 4668 array($this, 'enable_streaming_toggle_callback'),
6218 4669 'mxchat-chatbot',
@@ -6361,32 +4812,8 @@
6361 4812 'mxchat_chatbot_section'
6362 4813 );
6363 4814
6364 4815 add_settings_field(
6365 - 'print_button_enabled',
6366 - esc_html__('Show Download Transcript Button', 'mxchat'),
6367 - array($this, 'mxchat_print_button_toggle_callback'),
6368 - 'mxchat-chatbot',
6369 - 'mxchat_chatbot_section'
6370 - );
6371 -
6372 - add_settings_field(
6373 - 'reset_chat_enabled',
6374 - esc_html__('Show Start-New-Chat Button', 'mxchat'),
6375 - array($this, 'mxchat_reset_chat_toggle_callback'),
6376 - 'mxchat-chatbot',
6377 - 'mxchat_chatbot_section'
6378 - );
6379 -
6380 - add_settings_field(
6381 - 'reset_chat_label',
6382 - esc_html__('Start-New-Chat Button Label', 'mxchat'),
6383 - array($this, 'mxchat_reset_chat_label_callback'),
6384 - 'mxchat-chatbot',
6385 - 'mxchat_chatbot_section'
6386 - );
6387 -
6388 - add_settings_field(
6389 4816 'popular_question_1',
6390 4817 esc_html__('Quick Question 1', 'mxchat'),
6391 4818 array($this, 'mxchat_popular_question_1_callback'),
6392 4819 'mxchat-chatbot',
@@ -6776,16 +5203,8 @@
6776 5203 'mxchat_transcripts_notification_section'
6777 5204 );
6778 5205
6779 5206 add_settings_field(
6780 - 'mxchat_retention_days',
6781 - esc_html__('Custom Retention (Days)', 'mxchat'),
6782 - array($this, 'mxchat_retention_days_callback'),
6783 - 'mxchat-transcripts',
6784 - 'mxchat_transcripts_notification_section'
6785 - );
6786 -
6787 - add_settings_field(
6788 5207 'mxchat_auto_email_transcript',
6789 5208 esc_html__('Auto-Email Full Transcript', 'mxchat'),
6790 5209 array($this, 'mxchat_auto_email_transcript_callback'),
6791 5210 'mxchat-transcripts',
@@ -7022,9 +5441,8 @@
7022 5441 echo '<div class="api-key-wrapper">';
7023 5442 echo '<input type="text" id="api_key" name="api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7024 5443 echo '<button type="button" id="toggleApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7025 5444 echo '<p class="description">' . esc_html__('Required for OpenAI GPT models and OpenAI embeddings. Get your API key from OpenAI Platform.', 'mxchat') . '</p>';
7026 - echo $this->mxchat_provider_key_test_button('openai', 'api_key');
7027 5445 echo '</div>';
7028 5446 }
7029 5447
7030 5448 // X.AI API Key
@@ -7035,9 +5453,8 @@
7035 5453 echo '<div class="api-key-wrapper">';
7036 5454 echo '<input type="text" id="xai_api_key" name="xai_api_key" value="' . $xaiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7037 5455 echo '<button type="button" id="toggleXaiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7038 5456 echo '<p class="description">' . esc_html__('Required for X.AI Grok models. Get your API key from X.AI Console.', 'mxchat') . '</p>';
7039 - echo $this->mxchat_provider_key_test_button('xai', 'xai_api_key');
7040 5457 echo '</div>';
7041 5458 }
7042 5459 // Claude API Key
7043 5460 public function claude_api_key_callback() {
@@ -7047,9 +5464,8 @@
7047 5464 echo '<div class="api-key-wrapper">';
7048 5465 echo '<input type="text" id="claude_api_key" name="claude_api_key" value="' . $claudeApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7049 5466 echo '<button type="button" id="toggleClaudeApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7050 5467 echo '<p class="description">' . esc_html__('Required for Anthropic Claude models. Get your API key from Anthropic Console.', 'mxchat') . '</p>';
7051 - echo $this->mxchat_provider_key_test_button('claude', 'claude_api_key');
7052 5468 echo '</div>';
7053 5469 }
7054 5470
7055 5471 // DeepSeek API Key
@@ -7060,9 +5476,8 @@
7060 5476 echo '<div class="api-key-wrapper">';
7061 5477 echo '<input type="text" id="deepseek_api_key" name="deepseek_api_key" value="' . $apiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7062 5478 echo '<button type="button" id="toggleDeepSeekApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7063 5479 echo '<p class="description">' . esc_html__('Required for DeepSeek models. Get your API key from DeepSeek Platform.', 'mxchat') . '</p>';
7064 - echo $this->mxchat_provider_key_test_button('deepseek', 'deepseek_api_key');
7065 5480 echo '</div>';
7066 5481 }
7067 5482
7068 5483 // Gemini API Key
@@ -7073,9 +5488,8 @@
7073 5488 echo '<div class="api-key-wrapper">';
7074 5489 echo '<input type="text" id="gemini_api_key" name="gemini_api_key" value="' . $geminiApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7075 5490 echo '<button type="button" id="toggleGeminiApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7076 5491 echo '<p class="description">' . esc_html__('Required for Google Gemini models and embeddings. Get your API key from Google AI Studio.', 'mxchat') . '</p>';
7077 - echo $this->mxchat_provider_key_test_button('gemini', 'gemini_api_key');
7078 5492 echo '</div>';
7079 5493 }
7080 5494
7081 5495
@@ -7087,203 +5501,11 @@
7087 5501 echo '<div class="api-key-wrapper">';
7088 5502 echo '<input type="text" id="openrouter_api_key" name="openrouter_api_key" value="' . $openrouterApiKey . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7089 5503 echo '<button type="button" id="toggleOpenRouterApiKeyVisibility">' . esc_html__('Show', 'mxchat') . '</button>';
7090 5504 echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>';
7091 - echo $this->mxchat_provider_key_test_button('openrouter', 'openrouter_api_key');
7092 5505 echo '</div>';
7093 5506 }
7094 5507
7095 -/**
7096 - * Renders a "Test key" button + result target next to a built-in provider key
7097 - * field, and emits the shared delegated click handler ONCE (static guard). The
7098 - * button carries data-target = the key field id so the owner can test the value
7099 - * they just typed (test-before-save); the AJAX handler falls back to the saved
7100 - * key when the field is empty. Styled with the WP `button` class to match the
7101 - * adjacent Custom-provider "Test Connection" button on this same page.
7102 - * plan-mxchat-20260623-c41f74.
7103 - */
7104 -public function mxchat_provider_key_test_button($provider, $target_id) {
7105 - static $script_emitted = false;
7106 - $nonce = wp_create_nonce('mxchat_test_provider_key');
7107 - $html = '<div class="mxchat-key-test" style="margin-top:8px;">'
7108 - . '<button type="button" class="button mxchat-test-provider-key" data-provider="' . esc_attr($provider) . '" data-target="' . esc_attr($target_id) . '" data-nonce="' . esc_attr($nonce) . '">' . esc_html__('Test key', 'mxchat') . '</button>'
7109 - . '<span class="mxchat-test-provider-key-result" style="margin-left:10px;font-size:13px;vertical-align:middle;"></span>'
7110 - . '</div>';
7111 - if (!$script_emitted) {
7112 - $script_emitted = true;
7113 - $html .= $this->mxchat_provider_key_test_script();
7114 - }
7115 - return $html;
7116 -}
7117 -
7118 -/**
7119 - * One-time delegated click handler shared by every .mxchat-test-provider-key
7120 - * button. Posts the typed key value + provider to mxchat_test_provider_key and
7121 - * renders the success/error message inline. Mirrors the Custom-provider test.
7122 - */
7123 -private function mxchat_provider_key_test_script() {
7124 - $t_testing = esc_js(__('Testing...', 'mxchat'));
7125 - $t_valid = esc_js(__('Key is valid.', 'mxchat'));
7126 - $t_failed = esc_js(__('Failed', 'mxchat'));
7127 - $t_req = esc_js(__('Request failed', 'mxchat'));
7128 - return '<script>(function(){'
7129 - . 'if (window.__mxchatProviderKeyTestWired) { return; }'
7130 - . 'window.__mxchatProviderKeyTestWired = true;'
7131 - . 'document.addEventListener("click", function(e){'
7132 - . 'var btn = e.target && e.target.closest ? e.target.closest(".mxchat-test-provider-key") : null;'
7133 - . 'if (!btn) { return; }'
7134 - . 'e.preventDefault();'
7135 - . 'var field = btn.getAttribute("data-target") ? document.getElementById(btn.getAttribute("data-target")) : null;'
7136 - . 'var out = btn.parentNode ? btn.parentNode.querySelector(".mxchat-test-provider-key-result") : null;'
7137 - . 'if (out) { out.textContent = "' . $t_testing . '"; out.style.color = "#646970"; }'
7138 - . 'btn.disabled = true;'
7139 - . 'var fd = new FormData();'
7140 - . 'fd.append("action", "mxchat_test_provider_key");'
7141 - . 'fd.append("_wpnonce", btn.getAttribute("data-nonce"));'
7142 - . 'fd.append("provider", btn.getAttribute("data-provider") || "");'
7143 - . 'fd.append("key", field ? field.value : "");'
7144 - . 'fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })'
7145 - . '.then(function(r){ return r.json(); })'
7146 - . '.then(function(j){'
7147 - . 'if (out) {'
7148 - . 'if (j && j.success) { out.textContent = "✓ " + ((j.data && j.data.message) ? j.data.message : "' . $t_valid . '"); out.style.color = "#00a32a"; }'
7149 - . 'else { out.textContent = "⚠ " + ((j && j.data && j.data.message) ? j.data.message : "' . $t_failed . '"); out.style.color = "#d63638"; }'
7150 - . '}'
7151 - . 'btn.disabled = false;'
7152 - . '})'
7153 - . '.catch(function(){ if (out) { out.textContent = "⚠ ' . $t_req . '"; out.style.color = "#d63638"; } btn.disabled = false; });'
7154 - . '});'
7155 - . '})();</script>';
7156 -}
7157 -
7158 -// Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.
7159 -public function custom_provider_callback() {
7160 - $base_url = isset($this->options['custom_provider_base_url']) ? esc_attr($this->options['custom_provider_base_url']) : '';
7161 - $api_key = isset($this->options['custom_provider_api_key']) ? esc_attr($this->options['custom_provider_api_key']) : '';
7162 - $model_name = isset($this->options['custom_provider_model']) ? esc_attr($this->options['custom_provider_model']) : '';
7163 - $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? esc_attr($this->options['custom_provider_auth_scheme']) : 'bearer';
7164 - $api_version = isset($this->options['custom_provider_api_version']) ? esc_attr($this->options['custom_provider_api_version']) : '';
7165 - $use_embed = !empty($this->options['custom_provider_for_embeddings']) && $this->options['custom_provider_for_embeddings'] === 'on';
7166 - $use_images = !empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on';
7167 - $embed_model = isset($this->options['custom_provider_embedding_model']) ? esc_attr($this->options['custom_provider_embedding_model']) : '';
7168 - $nonce = wp_create_nonce('mxchat_autosave_nonce');
7169 - $test_nonce = wp_create_nonce('mxchat_test_custom_provider');
7170 -
7171 - echo '<style>
7172 - .mxchat-cp { max-width: 680px; }
7173 - .mxchat-cp .mxchat-cp-intro { margin: 0 0 16px; color: #50575e; font-size: 13px; line-height: 1.5; }
7174 - .mxchat-cp .mxchat-cp-row { display: block; margin: 0 0 18px; }
7175 - .mxchat-cp .mxchat-cp-row > label { display: block; font-weight: 600; margin: 0 0 6px; color: #1d2327; font-size: 13px; }
7176 - .mxchat-cp .mxchat-cp-row > input[type="text"],
7177 - .mxchat-cp .mxchat-cp-row > input[type="password"],
7178 - .mxchat-cp .mxchat-cp-row > select { display: block; width: 100%; max-width: 480px; margin: 0; }
7179 - .mxchat-cp .mxchat-cp-row > .description { display: block; margin: 6px 0 0; color: #646970; font-size: 12px; line-height: 1.5; max-width: 480px; }
7180 - .mxchat-cp .mxchat-cp-test { margin-top: 4px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
7181 - .mxchat-cp .mxchat-cp-test .mxchat-cp-test-status { display: inline-block; margin-left: 10px; vertical-align: middle; font-size: 13px; }
7182 - .mxchat-cp .mxchat-cp-azure { margin: 0 0 18px; padding: 12px 14px; background: #f6f7ff; border: 1px solid #dfe1f5; border-left: 3px solid #7873f5; border-radius: 6px; max-width: 480px; }
7183 - .mxchat-cp .mxchat-cp-azure > .mxchat-cp-azure-title { display: block; font-weight: 600; color: #1d2327; font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; margin: 0 0 8px; }
7184 - .mxchat-cp .mxchat-cp-azure ol { margin: 0; padding: 0 0 0 18px; color: #50575e; font-size: 12px; line-height: 1.7; }
7185 - .mxchat-cp .mxchat-cp-azure code { background: #eceefb; padding: 1px 5px; border-radius: 3px; font-size: 11px; }
7186 - </style>';
7187 -
7188 - echo '<div class="api-key-wrapper mxchat-cp">';
7189 - echo '<p class="mxchat-cp-intro">' . esc_html__('Point MxChat at any OpenAI-compatible /v1/chat/completions endpoint: Ollama, LM Studio, vLLM, llama.cpp, LocalAI, Azure OpenAI, etc. Then select "Custom (OpenAI-compatible)" in the model picker.', 'mxchat') . '</p>';
7190 -
7191 - // Azure OpenAI quick start — consolidates the 4-field Azure recipe in one scannable callout
7192 - // so admins can configure Azure without piecing it together from each field's hint.
7193 - echo '<div class="mxchat-cp-azure">';
7194 - echo '<span class="mxchat-cp-azure-title">' . esc_html__('Azure OpenAI quick start', 'mxchat') . '</span>';
7195 - echo '<ol>';
7196 - echo '<li>' . wp_kses(__('<strong>Base URL</strong> → <code>https://&lt;resource&gt;.openai.azure.com/openai/deployments/&lt;deployment&gt;</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7197 - echo '<li>' . wp_kses(__('<strong>API Key</strong> → your Azure OpenAI key (required)', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7198 - echo '<li>' . wp_kses(__('<strong>Auth Scheme</strong> → <code>api-key header (Azure OpenAI)</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7199 - echo '<li>' . wp_kses(__('<strong>API Version</strong> → required for Azure, e.g. <code>2024-08-01-preview</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>';
7200 - echo '</ol>';
7201 - echo '</div>';
7202 -
7203 - echo '<div class="mxchat-cp-row">';
7204 - echo '<label for="custom_provider_base_url">' . esc_html__('Base URL', 'mxchat') . '</label>';
7205 - echo '<input type="text" id="custom_provider_base_url" name="custom_provider_base_url" value="' . $base_url . '" class="regular-text mxchat-autosave-field" placeholder="http://localhost:11434/v1" autocomplete="off" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7206 - echo '<p class="description">' . esc_html__('Examples: Ollama http://localhost:11434/v1 · LM Studio http://localhost:1234/v1 · vLLM http://gpu:8000/v1 · Azure https://<resource>.openai.azure.com/openai/deployments/<deployment>', 'mxchat') . '</p>';
7207 - echo '</div>';
7208 -
7209 - echo '<div class="mxchat-cp-row">';
7210 - echo '<label for="custom_provider_api_key">' . esc_html__('API Key (optional)', 'mxchat') . '</label>';
7211 - echo '<input type="password" id="custom_provider_api_key" name="custom_provider_api_key" value="' . $api_key . '" class="regular-text mxchat-autosave-field mxchat-api-key-field" autocomplete="new-password" data-lpignore="true" data-form-type="other" data-nonce="' . $nonce . '" />';
7212 - echo '<p class="description">' . esc_html__('Leave empty for unauthenticated local servers. Required for Azure / vLLM / hosted endpoints.', 'mxchat') . '</p>';
7213 - echo '</div>';
7214 -
7215 - echo '<div class="mxchat-cp-row">';
7216 - echo '<label for="custom_provider_model">' . esc_html__('Model Name', 'mxchat') . '</label>';
7217 - echo '<input type="text" id="custom_provider_model" name="custom_provider_model" value="' . $model_name . '" class="regular-text mxchat-autosave-field" placeholder="llama3.2" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7218 - echo '<p class="description">' . esc_html__('The model identifier the upstream server expects (e.g. llama3.2, mistral, gpt-oss). For Azure this is the deployment ID — leave empty if the Base URL already includes /deployments/<deployment>.', 'mxchat') . '</p>';
7219 - echo '</div>';
7220 -
7221 - echo '<div class="mxchat-cp-row">';
7222 - echo '<label for="custom_provider_auth_scheme">' . esc_html__('Auth Scheme', 'mxchat') . '</label>';
7223 - echo '<select id="custom_provider_auth_scheme" name="custom_provider_auth_scheme" class="mxchat-autosave-field" data-nonce="' . $nonce . '">';
7224 - echo '<option value="bearer"' . selected($auth_scheme, 'bearer', false) . '>' . esc_html__('Authorization: Bearer (OpenAI / Ollama / vLLM / LM Studio)', 'mxchat') . '</option>';
7225 - echo '<option value="api-key"' . selected($auth_scheme, 'api-key', false) . '>' . esc_html__('api-key header (Azure OpenAI)', 'mxchat') . '</option>';
7226 - echo '</select>';
7227 - echo '<p class="description">' . esc_html__('Most OpenAI-compatible servers use Bearer. Azure OpenAI uses the api-key header.', 'mxchat') . '</p>';
7228 - echo '</div>';
7229 -
7230 - echo '<div class="mxchat-cp-row">';
7231 - echo '<label for="custom_provider_api_version">' . esc_html__('API Version (Azure only)', 'mxchat') . '</label>';
7232 - echo '<input type="text" id="custom_provider_api_version" name="custom_provider_api_version" value="' . $api_version . '" class="regular-text mxchat-autosave-field" placeholder="2024-08-01-preview" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7233 - echo '<p class="description">' . esc_html__('Appended as ?api-version=... on the request URL. Required for Azure OpenAI; leave empty for non-Azure providers.', 'mxchat') . '</p>';
7234 - echo '</div>';
7235 -
7236 - // Extended-use checkboxes — opt-in routing of other dispatcher paths through the custom provider.
7237 - echo '<div class="mxchat-cp-row">';
7238 - echo '<label style="font-weight:600; display:block; margin:0 0 6px; color:#1d2327; font-size:13px;">' . esc_html__('Extended routing (opt-in)', 'mxchat') . '</label>';
7239 - echo '<label style="display:block; margin:0 0 6px; font-weight:400;"><input type="checkbox" id="custom_provider_for_embeddings" name="custom_provider_for_embeddings" value="on"' . checked($use_embed, true, false) . ' class="mxchat-autosave-field" data-nonce="' . $nonce . '" /> ' . esc_html__('Use custom provider for embeddings', 'mxchat') . '</label>';
7240 - echo '<label style="display:block; margin:0 0 0; font-weight:400;"><input type="checkbox" id="custom_provider_for_images" name="custom_provider_for_images" value="on"' . checked($use_images, true, false) . ' class="mxchat-autosave-field" data-nonce="' . $nonce . '" /> ' . esc_html__('Use custom provider for image generation', 'mxchat') . '</label>';
7241 - echo '<p class="description">' . esc_html__('When off, embeddings and image generation continue to use OpenAI (current behavior). Turn on only if your endpoint exposes OpenAI-compatible /embeddings or /images/generations routes (e.g. Ollama, vLLM, LocalAI).', 'mxchat') . '</p>';
7242 - echo '</div>';
7243 -
7244 - echo '<div class="mxchat-cp-row">';
7245 - echo '<label for="custom_provider_embedding_model">' . esc_html__('Custom Embedding Model', 'mxchat') . '</label>';
7246 - echo '<input type="text" id="custom_provider_embedding_model" name="custom_provider_embedding_model" value="' . $embed_model . '" class="regular-text mxchat-autosave-field" placeholder="nomic-embed-text" autocomplete="off" data-lpignore="true" data-nonce="' . $nonce . '" />';
7247 - echo '<p class="description">' . esc_html__('Only used when "Use custom provider for embeddings" is on. The embedding model name is separate from the chat model name above (e.g. Ollama embedding models: nomic-embed-text, mxbai-embed-large). Leave blank to fall back to the chat model name.', 'mxchat') . '</p>';
7248 - echo '</div>';
7249 -
7250 - echo '<div class="mxchat-cp-test">';
7251 - echo '<button type="button" class="button" id="mxchat-test-custom-provider" data-nonce="' . $test_nonce . '">' . esc_html__('Test Connection', 'mxchat') . '</button>';
7252 - echo '<span id="mxchat-test-custom-provider-result" class="mxchat-cp-test-status"></span>';
7253 - echo '</div>';
7254 -
7255 - echo '<script>(function(){
7256 - var btn = document.getElementById("mxchat-test-custom-provider");
7257 - if (!btn || btn._wired) { return; } btn._wired = true;
7258 - btn.addEventListener("click", function(){
7259 - var out = document.getElementById("mxchat-test-custom-provider-result");
7260 - out.textContent = "' . esc_js(__('Testing...', 'mxchat')) . '";
7261 - out.style.color = "#646970";
7262 - var fd = new FormData();
7263 - fd.append("action", "mxchat_test_custom_provider");
7264 - fd.append("_wpnonce", btn.getAttribute("data-nonce"));
7265 - fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd })
7266 - .then(function(r){ return r.json(); })
7267 - .then(function(j){
7268 - if (j && j.success) {
7269 - out.textContent = "✓ " + (j.data && j.data.message ? j.data.message : "' . esc_js(__('OK', 'mxchat')) . '");
7270 - out.style.color = "#00a32a";
7271 - } else {
7272 - out.textContent = "⚠ " + (j && j.data && j.data.message ? j.data.message : "' . esc_js(__('Failed', 'mxchat')) . '");
7273 - out.style.color = "#d63638";
7274 - }
7275 - })
7276 - .catch(function(){
7277 - out.textContent = "⚠ ' . esc_js(__('Request failed', 'mxchat')) . '";
7278 - out.style.color = "#d63638";
7279 - });
7280 - });
7281 - })();</script>';
7282 -
7283 - echo '</div>';
7284 -}
7285 -
7286 5508 // Voyage API Key
7287 5509 public function voyage_api_key_callback() {
7288 5510 $apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : '';
7289 5511 $nonce = wp_create_nonce('mxchat_autosave_nonce');
@@ -7492,15 +5714,56 @@
7492 5714 }
7493 5715
7494 5716
7495 5717 public function mxchat_model_callback() {
7496 - // Catalog refactor (plan-d14e89): single source of truth lives in
7497 - // includes/class-mxchat-model-catalog.php. Dropdown groups are the
7498 - // provider labels; each group maps model_id => "Label" strings.
7499 - if (!class_exists('MxChat_Model_Catalog')) {
7500 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7501 - }
7502 - $models = MxChat_Model_Catalog::settings_dropdown_groups();
5718 + // Define available models grouped by provider
5719 + $models = array(
5720 + esc_html__('OpenRouter (100+ Models)', 'mxchat') => array(
5721 + 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'),
5722 + ),
5723 + esc_html__('Google Gemini Models', 'mxchat') => array(
5724 + 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'),
5725 + 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'),
5726 + 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'),
5727 + 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'),
5728 + 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'),
5729 + 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'),
5730 + 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'),
5731 + 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'),
5732 + 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'),
5733 + ),
5734 + esc_html__('X.AI Models', 'mxchat') => array(
5735 + 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'),
5736 + 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'),
5737 + 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'),
5738 + 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'),
5739 + 'grok-2' => esc_html__('Grok 2', 'mxchat'),
5740 + 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'),
5741 + 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'),
5742 + 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'),
5743 + ),
5744 + esc_html__('DeepSeek Models', 'mxchat') => array(
5745 + 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'),
5746 + ),
5747 + esc_html__('Claude Models', 'mxchat') => array(
5748 + 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'),
5749 + 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'),
5750 + 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'),
5751 + 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'),
5752 + 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'),
5753 + 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'),
5754 + 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'),
5755 + 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'),
5756 + ),
5757 + esc_html__('OpenAI Models', 'mxchat') => array(
5758 + 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'),
5759 + 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'),
5760 + 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'),
5761 + 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'),
5762 + 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'),
5763 + 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'),
5764 + ),
5765 + );
7503 5766
7504 5767 // Retrieve the currently selected model from saved options
7505 5768 $selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest';
7506 5769
@@ -7609,15 +5872,15 @@
7609 5872 );
7610 5873 echo '<span class="slider"></span>';
7611 5874 echo '</label>';
7612 5875
7613 - // Test button — branded .mxch-btn with inline SVG (IDs preserved for AJAX binding)
7614 - echo '<div class="mxch-streaming-test-row">';
7615 - echo '<button type="button" id="mxchat-test-streaming-btn" class="mxch-btn mxch-btn-secondary">';
7616 - echo '<svg class="mxch-streaming-test-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
5876 + // Test button with better styling
5877 + echo '<div style="margin-top: 15px;">';
5878 + echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">';
5879 + echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>';
7617 5880 echo esc_html__('Test Streaming Compatibility', 'mxchat');
7618 5881 echo '</button>';
7619 - echo '<p id="mxchat-test-streaming-result" class="mxch-streaming-test-result"></p>';
5882 + echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>';
7620 5883 echo '</div>';
7621 5884 }
7622 5885
7623 5886 // Web Search toggle callback
@@ -7628,37 +5891,22 @@
7628 5891
7629 5892 // Get current model to determine if we should show/enable the toggle
7630 5893 $current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
7631 5894
7632 - // Models that DON'T support web search — OpenAI-docs-driven exception list.
7633 - // Keep hardcoded; the catalog can't infer "supports web search" per-model, so any
7634 - // future OpenAI model that lacks Responses-API web_search support is added here.
5895 + // Models that DON'T support web search (per OpenAI docs)
5896 + // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it
7635 5897 $unsupported_models = array('gpt-4.1-nano');
7636 5898
7637 - // Web-search-capable chat-model allowlists, derived from the central model catalog
7638 - // (class-mxchat-model-catalog.php). When a new OpenAI/Gemini chat model is added there,
7639 - // the Web Search toggle picks it up automatically — no edit here.
7640 - // OpenAI grounds via the Responses-API web_search tool; Gemini grounds natively via the
7641 - // Google Search tool (plan 46b9ea wired the Gemini dispatch — every shipped Gemini chat
7642 - // model is 2.x/3.x and grounds, matching that path's empty opt-out list, so all catalog
7643 - // Gemini models are supported here).
7644 - if (!class_exists('MxChat_Model_Catalog')) {
7645 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
7646 - }
7647 - $chat_catalog = MxChat_Model_Catalog::chat_models();
7648 - $openai_models = (isset($chat_catalog['openai']['models']) && is_array($chat_catalog['openai']['models']))
7649 - ? array_keys($chat_catalog['openai']['models'])
7650 - : array();
7651 - $gemini_models = (isset($chat_catalog['gemini']['models']) && is_array($chat_catalog['gemini']['models']))
7652 - ? array_keys($chat_catalog['gemini']['models'])
7653 - : array();
5899 + // Check if current model is an OpenAI model that supports web search
5900 + $openai_models = array(
5901 + 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'
5902 + );
7654 5903
7655 - $is_capable = in_array($current_model, $openai_models) || in_array($current_model, $gemini_models);
7656 - $is_supported = $is_capable && !in_array($current_model, $unsupported_models);
5904 + $is_openai = in_array($current_model, $openai_models);
5905 + $is_supported = $is_openai && !in_array($current_model, $unsupported_models);
7657 5906
7658 - // Wrapper div with data attributes for JS to show/hide. data-openai-models is kept for
7659 - // back-compat; data-gemini-models is the added second provider the JS now also honors.
7660 - echo '<div id="web-search-toggle-wrapper" data-openai-models="' . esc_attr(implode(',', $openai_models)) . '" data-gemini-models="' . esc_attr(implode(',', $gemini_models)) . '" data-unsupported-models="' . esc_attr(implode(',', $unsupported_models)) . '"' . (!$is_supported ? ' style="display:none;"' : '') . '>';
5907 + // Wrapper div with data attributes for JS to show/hide
5908 + echo '<div id="web-search-toggle-wrapper" data-openai-models="' . esc_attr(implode(',', $openai_models)) . '" data-unsupported-models="' . esc_attr(implode(',', $unsupported_models)) . '"' . (!$is_supported ? ' style="display:none;"' : '') . '>';
7661 5909
7662 5910 echo '<label class="toggle-switch">';
7663 5911 echo sprintf(
7664 5912 '<input type="checkbox" id="enable_web_search" name="enable_web_search" value="on" %s />',
@@ -7668,12 +5916,12 @@
7668 5916 echo '</label>';
7669 5917
7670 5918 echo '</div>';
7671 5919
7672 - // Message shown when a model that can't ground (Claude, Grok, DeepSeek, OpenRouter, etc.) is selected
5920 + // Message shown when non-OpenAI model is selected
7673 5921 echo '<p id="web-search-unavailable-message" class="description" style="color: #666;' . ($is_supported ? ' display:none;' : '') . '">';
7674 5922 echo '<span class="dashicons dashicons-info" style="font-size: 16px; vertical-align: middle; margin-right: 4px;"></span>';
7675 - echo esc_html__('Web search is only available for OpenAI and Gemini models.', 'mxchat');
5923 + echo esc_html__('Web search is only available for OpenAI models.', 'mxchat');
7676 5924 echo '</p>';
7677 5925 }
7678 5926
7679 5927 // AJAX handler to fetch OpenRouter models
@@ -7778,9 +6026,8 @@
7778 6026 } else {
7779 6027 echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>';
7780 6028 }
7781 6029 echo '</p>';
7782 -
7783 6030 }
7784 6031
7785 6032
7786 6033 public function mxchat_top_bar_title_callback() {
@@ -7882,12 +6129,11 @@
7882 6129 $all_options = get_option('mxchat_options', []);
7883 6130 // Retrieve the saved intro message or use the default
7884 6131 $default_message = __('Hello! How can I assist you today?', 'mxchat');
7885 6132 $saved_message = isset($all_options['intro_message']) ? $all_options['intro_message'] : $default_message;
7886 - // Escape on output (esc_textarea) — neutralizes any payload already stored before the
7887 - // Wordfence Stored-XSS fix (CWE-79, plan-3f8158) and prevents </textarea> context-breakout.
6133 + // Output the textarea with the saved value without escaping HTML
7888 6134 ?>
7889 - <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo esc_textarea( $saved_message ); ?></textarea>
6135 + <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea>
7890 6136 <p class="description" style="margin-top: 8px;">
7891 6137 <?php esc_html_e('Use {visitor_name} to personalize greetings when lead capture is enabled.', 'mxchat'); ?><br>
7892 6138 <code style="font-size: 12px;"><?php esc_html_e('Example: Hello {visitor_name}! How can I help you today?', 'mxchat'); ?></code>
7893 6139 </p>
@@ -7990,8 +6236,20 @@
7990 6236 echo '</div>'; // End post-type-visibility-options
7991 6237 echo '</div>'; // End mxchat-autosave-section
7992 6238 }
7993 6239
6240 +public function mxchat_frontend_debugger_callback() {
6241 + $show_debugger = isset($this->options['show_frontend_debugger']) ? $this->options['show_frontend_debugger'] : 'on';
6242 + $checked = ($show_debugger === 'on') ? 'checked' : '';
6243 + echo '<label class="toggle-switch">';
6244 + echo sprintf(
6245 + '<input type="checkbox" id="show_frontend_debugger" name="show_frontend_debugger" value="on" %s />',
6246 + esc_attr($checked)
6247 + );
6248 + echo '<span class="slider"></span>';
6249 + echo '</label>';
6250 +}
6251 +
7994 6252 public function mxchat_contextual_awareness_callback() {
7995 6253 // Get value from options array, default to 'off'
7996 6254 $contextual_awareness = isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off';
7997 6255 $checked = ($contextual_awareness === 'on') ? 'checked' : '';
@@ -8016,125 +6274,8 @@
8016 6274 echo '<span class="slider"></span>';
8017 6275 echo '</label>';
8018 6276 }
8019 6277
8020 -/**
8021 - * Toggle for the end-of-session satisfaction rating prompt (plan-a5b006).
8022 - * Default ON. The widget reads this through the localized object; the
8023 - * mxchat_satisfaction_rating_enabled filter still lets developers force
8024 - * the value site-wide.
8025 - *
8026 - * The 5 customization fields (idle/question/thanks/placeholder/saved) are
8027 - * rendered inline here inside a single wrapper div whose initial display
8028 - * is set server-side from the toggle value (plan-29caac). Mirrors the
8029 - * auto-display chatbot pattern at mxchat_append_to_body_callback — no
8030 - * DOMContentLoaded race because rows exist as direct children of this
8031 - * callback's output, and the wrapper's display: none is inline at render
8032 - * time so refresh shows the correct state with no flash.
8033 - */
8034 -public function mxchat_satisfaction_rating_toggle_callback() {
8035 - $options = $this->options;
8036 - $value = isset($options['satisfaction_rating_enabled']) ? $options['satisfaction_rating_enabled'] : 'off';
8037 - $checked = ($value === 'on') ? 'checked' : '';
8038 -
8039 - $idle = isset($options['satisfaction_rating_idle_seconds']) ? intval($options['satisfaction_rating_idle_seconds']) : 60;
8040 - $idle = max(5, min(600, $idle));
8041 - $question = isset($options['satisfaction_rating_question']) ? $options['satisfaction_rating_question'] : '';
8042 - $thanks = isset($options['satisfaction_rating_thanks']) ? $options['satisfaction_rating_thanks'] : '';
8043 - $placeholder = isset($options['satisfaction_rating_placeholder']) ? $options['satisfaction_rating_placeholder'] : '';
8044 - $saved = isset($options['satisfaction_rating_saved']) ? $options['satisfaction_rating_saved'] : '';
8045 -
8046 - echo '<div class="mxchat-autosave-section">';
8047 -
8048 - echo '<label class="toggle-switch">';
8049 - echo sprintf(
8050 - '<input type="checkbox" id="satisfaction_rating_enabled" name="satisfaction_rating_enabled" value="on" %s />',
8051 - esc_attr($checked)
8052 - );
8053 - echo '<span class="slider"></span>';
8054 - echo '</label>';
8055 -
8056 - ?>
8057 - <style>
8058 - .mxchat-sub-options-field { margin: 12px 0; }
8059 - .mxchat-sub-options-field label { display: inline-block; margin-bottom: 4px; }
8060 - #satisfaction-rating-sub-options h4 { margin: 16px 0 8px; }
8061 - </style>
8062 - <?php
8063 -
8064 - $display_style = ($value === 'on') ? '' : 'display: none;';
8065 - echo '<div id="satisfaction-rating-sub-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">';
8066 -
8067 - echo '<h4>' . esc_html__('Customize the prompt (optional)', 'mxchat') . '</h4>';
8068 -
8069 - echo '<div class="mxchat-sub-options-field">';
8070 - echo '<label for="satisfaction_rating_idle_seconds"><strong>' . esc_html__('Idle Timeout', 'mxchat') . '</strong></label><br />';
8071 - printf(
8072 - '<input type="number" id="satisfaction_rating_idle_seconds" name="satisfaction_rating_idle_seconds" value="%d" min="5" max="600" step="1" class="small-text" /> <span class="description">%s</span>',
8073 - (int) $idle,
8074 - esc_html__('seconds of user inactivity before the prompt appears (5-600)', 'mxchat')
8075 - );
8076 - echo '</div>';
8077 -
8078 - echo '<div class="mxchat-sub-options-field">';
8079 - echo '<label for="satisfaction_rating_question"><strong>' . esc_html__('Prompt Question', 'mxchat') . '</strong></label><br />';
8080 - printf(
8081 - '<input type="text" id="satisfaction_rating_question" name="satisfaction_rating_question" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8082 - esc_attr($question),
8083 - esc_attr__('Was this helpful?', 'mxchat')
8084 - );
8085 - echo '<p class="description">' . esc_html__('Leave blank for the default. Shown above the thumbs up/down.', 'mxchat') . '</p>';
8086 - echo '</div>';
8087 -
8088 - echo '<div class="mxchat-sub-options-field">';
8089 - echo '<label for="satisfaction_rating_thanks"><strong>' . esc_html__('Thank-You Message', 'mxchat') . '</strong></label><br />';
8090 - printf(
8091 - '<input type="text" id="satisfaction_rating_thanks" name="satisfaction_rating_thanks" value="%s" maxlength="300" class="regular-text" placeholder="%s" />',
8092 - esc_attr($thanks),
8093 - esc_attr__('Thanks! Anything we should improve? (optional)', 'mxchat')
8094 - );
8095 - echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the user clicks a thumb.', 'mxchat') . '</p>';
8096 - echo '</div>';
8097 -
8098 - echo '<div class="mxchat-sub-options-field">';
8099 - echo '<label for="satisfaction_rating_placeholder"><strong>' . esc_html__('Feedback Placeholder', 'mxchat') . '</strong></label><br />';
8100 - printf(
8101 - '<input type="text" id="satisfaction_rating_placeholder" name="satisfaction_rating_placeholder" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8102 - esc_attr($placeholder),
8103 - esc_attr__('Tell us what could be better…', 'mxchat')
8104 - );
8105 - echo '<p class="description">' . esc_html__('Leave blank for the default. Placeholder text inside the feedback textarea.', 'mxchat') . '</p>';
8106 - echo '</div>';
8107 -
8108 - echo '<div class="mxchat-sub-options-field">';
8109 - echo '<label for="satisfaction_rating_saved"><strong>' . esc_html__('Saved Confirmation', 'mxchat') . '</strong></label><br />';
8110 - printf(
8111 - '<input type="text" id="satisfaction_rating_saved" name="satisfaction_rating_saved" value="%s" maxlength="200" class="regular-text" placeholder="%s" />',
8112 - esc_attr($saved),
8113 - esc_attr__('Thanks for the feedback.', 'mxchat')
8114 - );
8115 - echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the feedback is sent.', 'mxchat') . '</p>';
8116 - echo '</div>';
8117 -
8118 - echo '</div>'; // #satisfaction-rating-sub-options
8119 - echo '</div>'; // .mxchat-autosave-section
8120 -
8121 - ?>
8122 - <script>
8123 - (function() {
8124 - document.addEventListener('DOMContentLoaded', function() {
8125 - var toggle = document.getElementById('satisfaction_rating_enabled');
8126 - var subOptions = document.getElementById('satisfaction-rating-sub-options');
8127 - if (!toggle || !subOptions) return;
8128 - toggle.addEventListener('change', function() {
8129 - subOptions.style.display = toggle.checked ? '' : 'none';
8130 - });
8131 - });
8132 - })();
8133 - </script>
8134 - <?php
8135 -}
8136 -
8137 6278 public function mxchat_privacy_toggle_callback() {
8138 6279 // Load from mxchat_options array
8139 6280 $options = get_option('mxchat_options', []);
8140 6281
@@ -8217,53 +6358,8 @@
8217 6358 echo '<span class="slider"></span>';
8218 6359 echo '</label>';
8219 6360 }
8220 6361
8221 -public function mxchat_print_button_toggle_callback() {
8222 - // Load from mxchat_options array
8223 - $options = get_option('mxchat_options', []);
8224 -
8225 - // Default ON — the option was previously unexposed and the button always showed.
8226 - $print_button_enabled = isset($options['print_button_enabled']) ? $options['print_button_enabled'] : 'on';
8227 - $checked = ($print_button_enabled === 'on') ? 'checked' : '';
8228 -
8229 - // Output the toggle switch
8230 - echo '<label class="toggle-switch">';
8231 - echo sprintf(
8232 - '<input type="checkbox" id="print_button_enabled" name="print_button_enabled" value="on" %s />',
8233 - esc_attr($checked)
8234 - );
8235 - echo '<span class="slider"></span>';
8236 - echo '</label>';
8237 -}
8238 -
8239 -public function mxchat_reset_chat_toggle_callback() {
8240 - // Load from mxchat_options array. plan ac2e81 — default OFF (new, opt-in).
8241 - $options = get_option('mxchat_options', []);
8242 - $reset_chat_enabled = isset($options['reset_chat_enabled']) ? $options['reset_chat_enabled'] : 'off';
8243 - $checked = ($reset_chat_enabled === 'on') ? 'checked' : '';
8244 -
8245 - echo '<label class="toggle-switch">';
8246 - echo sprintf(
8247 - '<input type="checkbox" id="reset_chat_enabled" name="reset_chat_enabled" value="on" %s />',
8248 - esc_attr($checked)
8249 - );
8250 - echo '<span class="slider"></span>';
8251 - echo '</label>';
8252 -}
8253 -
8254 -public function mxchat_reset_chat_label_callback() {
8255 - // Editable label for the "Start new chat" menu item. plan ac2e81.
8256 - $options = get_option('mxchat_options', []);
8257 - $reset_chat_label = isset($options['reset_chat_label']) ? $options['reset_chat_label'] : '';
8258 -
8259 - printf(
8260 - '<input type="text" id="reset_chat_label" name="reset_chat_label" value="%s" placeholder="%s" class="regular-text" />',
8261 - esc_attr($reset_chat_label),
8262 - esc_attr__('Start new chat', 'mxchat')
8263 - );
8264 -}
8265 -
8266 6362 public function mxchat_popular_question_1_callback() {
8267 6363 // Load the full plugin options array
8268 6364 $all_options = get_option('mxchat_options', []);
8269 6365
@@ -8795,35 +6891,14 @@
8795 6891 );
8796 6892 echo '</div>';
8797 6893 }
8798 6894
8799 -public function mxchat_max_input_length_callback() {
8800 - // Load from mxchat_options array (plan a3fae2 part C).
8801 - $options = get_option('mxchat_options', []);
8802 -
8803 - // 0 = unlimited (default — preserves current behavior, no cap).
8804 - $max_input_length = isset($options['max_input_length']) ? intval($options['max_input_length']) : 0;
8805 -
8806 - echo sprintf(
8807 - '<input type="number"
8808 - id="max_input_length"
8809 - name="max_input_length"
8810 - min="0"
8811 - max="100000"
8812 - step="1"
8813 - value="%s"
8814 - placeholder="0"
8815 - class="mxch-input mxch-input-sm" />',
8816 - esc_attr($max_input_length)
8817 - );
8818 -}
8819 -
8820 6895 public function mxchat_rag_sources_limit_callback() {
8821 6896 // Load from mxchat_options array
8822 6897 $options = get_option('mxchat_options', []);
8823 6898
8824 - // Get value from options array with default of 3
8825 - $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 3;
6899 + // Get value from options array with default of 6
6900 + $rag_sources_limit = isset($options['rag_sources_limit']) ? intval($options['rag_sources_limit']) : 6;
8826 6901
8827 6902 echo '<div class="slider-container">';
8828 6903 echo sprintf(
8829 6904 '<input type="range"
@@ -8842,46 +6917,8 @@
8842 6917 );
8843 6918 echo '</div>';
8844 6919 }
8845 6920
8846 -public function mxchat_rag_chunks_limit_callback() {
8847 - // Load from mxchat_options array
8848 - $options = get_option('mxchat_options', []);
8849 -
8850 - // Get value from options array with default of 15
8851 - $rag_chunks_limit = isset($options['rag_chunks_limit']) ? intval($options['rag_chunks_limit']) : 15;
8852 -
8853 - echo '<div class="slider-container">';
8854 - echo sprintf(
8855 - '<input type="range"
8856 - id="rag_chunks_limit"
8857 - name="rag_chunks_limit"
8858 - min="8"
8859 - max="20"
8860 - step="1"
8861 - value="%s"
8862 - class="range-slider" />',
8863 - esc_attr($rag_chunks_limit)
8864 - );
8865 - echo sprintf(
8866 - '<span id="rag_chunks_limit_value" class="range-value">%s</span>',
8867 - esc_html($rag_chunks_limit)
8868 - );
8869 - echo '</div>';
8870 -}
8871 -
8872 -/**
8873 - * Add body class on the Onboarding wizard page so CSS-only chrome surgery
8874 - * (collapsing the WP admin sidebar) only applies on this page.
8875 - * Plan: plan-mxchat-20260527-905439.
8876 - */
8877 -public function mxchat_add_onboarding_body_class($classes) {
8878 - if (isset($_GET['page']) && $_GET['page'] === 'mxchat-onboarding') {
8879 - $classes .= ' mxchat-onboarding-focused';
8880 - }
8881 - return $classes;
8882 -}
8883 -
8884 6921 public function mxchat_enqueue_admin_assets() {
8885 6922 // Get plugin version
8886 6923 $version = MXCHAT_VERSION;
8887 6924
@@ -8921,13 +6958,8 @@
8921 6958 wp_enqueue_style('mxchat-content-selector-css', $plugin_url . 'css/content-selector.css', array(), $version);
8922 6959 wp_enqueue_script('mxchat-content-selector-js', $plugin_url . 'js/content-selector.js', array('jquery'), $version, true);
8923 6960 // Add the knowledge processing script (common script needed for WordPress dismiss functionality)
8924 6961 wp_enqueue_script('mxchat-knowledge-processing', $plugin_url . 'js/knowledge-processing.js', array('jquery', 'common'), $version, true);
8925 - // Per-entry "View indexed content" inspector (plan-d8cb4b) reuses the
8926 - // Testing tab's match-card / chunk-detail components, which are scoped
8927 - // under .mxch-testing-results. Load that stylesheet here so the inspector
8928 - // modal renders identically to the Testing tab.
8929 - wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css'), $version);
8930 6962 break;
8931 6963
8932 6964 case 'mxchat-transcripts':
8933 6965 // Load admin sidebar CSS first (shared styles for sidebar navigation)
@@ -8952,16 +6984,12 @@
8952 6984 'addNonce' => wp_create_nonce('mxchat_add_intent_nonce'),
8953 6985 'editNonce' => wp_create_nonce('mxchat_edit_intent'),
8954 6986 'deleteNonce' => wp_create_nonce('mxchat_delete_intent_nonce'),
8955 6987 'toggleNonce' => wp_create_nonce('mxchat_actions_nonce'),
8956 - 'addPhraseNonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
8957 - 'deletePhraseNonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
8958 - 'getPhrasesNonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
8959 - 'deleteLegacyNonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
8960 6988 'isActivated' => $is_activated,
8961 6989 'i18n' => array(
8962 - 'confirmDelete' => __('Are you sure you want to delete this trigger phrase?', 'mxchat'),
8963 - 'confirmBulkDelete' => __('Are you sure you want to delete the selected trigger phrases?', 'mxchat'),
6990 + 'confirmDelete' => __('Are you sure you want to delete this action?', 'mxchat'),
6991 + 'confirmBulkDelete' => __('Are you sure you want to delete the selected actions?', 'mxchat'),
8964 6992 'saving' => __('Saving...', 'mxchat'),
8965 6993 'saved' => __('Saved successfully!', 'mxchat'),
8966 6994 'error' => __('An error occurred. Please try again.', 'mxchat'),
8967 6995 'proRequired' => __('This feature requires MxChat Pro.', 'mxchat'),
@@ -8988,38 +7016,8 @@
8988 7016 wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version);
8989 7017 // Load content page JavaScript
8990 7018 wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true);
8991 7019 break;
8992 -
8993 - case 'mxchat-api-access':
8994 - // Shared sidebar shell (CSS + JS for tab switching / mobile menu / copy buttons).
8995 - wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
8996 - wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
8997 - wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
8998 - 'copied' => __('Copied', 'mxchat'),
8999 - ));
9000 - break;
9001 -
9002 - case 'mxchat-max':
9003 - case 'mxchat-onboarding':
9004 - // Onboarding page — uses the shared admin shell PLUS the wizard
9005 - // overlay (plan-905439). admin-onboarding-wizard.css scopes the
9006 - // WP-chrome surgery to body.mxchat-onboarding-focused so it only
9007 - // applies on THIS page. The body class is added below via the
9008 - // admin_body_class filter.
9009 - wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version);
9010 - wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true);
9011 - wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array(
9012 - 'copied' => __('Copied', 'mxchat'),
9013 - ));
9014 - // admin-style.css provides the .mxchat-instructions-modal-* classes
9015 - // the new Behavior step's "View Sample Instructions" modal needs.
9016 - // Enqueued AFTER admin-sidebar.css but BEFORE the wizard overlay
9017 - // so the wizard's own rules win where they collide. plan-a2e4d6.
9018 - wp_enqueue_style('mxchat-admin-style-css', $plugin_url . 'css/admin-style.css', array('mxchat-admin-sidebar-css'), $version);
9019 - wp_enqueue_style('mxchat-admin-onboarding-wizard-css', $plugin_url . 'css/admin-onboarding-wizard.css', array('mxchat-admin-sidebar-css', 'mxchat-admin-style-css'), $version);
9020 - wp_enqueue_script('mxchat-admin-onboarding-wizard-js', $plugin_url . 'js/admin-onboarding-wizard.js', array(), $version, true);
9021 - break;
9022 7020 default:
9023 7021 wp_enqueue_script(
9024 7022 'mxchat-test-streaming-js',
9025 7023 $plugin_url . 'js/mxchat-test-streaming.js',
@@ -9032,77 +7030,8 @@
9032 7030 'ajax_url' => admin_url('admin-ajax.php'),
9033 7031 'nonce' => wp_create_nonce('mxchat_test_streaming_nonce'),
9034 7032 'settings_nonce' => wp_create_nonce('mxchat_save_setting_nonce')
9035 7033 ]);
9036 -
9037 - // Testing Tab: Load chatbot assets for the embedded testing chatbot
9038 - wp_enqueue_style('mxchat-chat-css', $plugin_url . 'css/chat-style.css', array(), $version);
9039 - wp_enqueue_style('mxchat-admin-testing-css', $plugin_url . 'css/admin-testing-tab.css', array('mxchat-admin-sidebar-css', 'mxchat-chat-css'), $version);
9040 -
9041 - wp_enqueue_script('mxchat-chat-js', $plugin_url . 'js/chat-script.js', array('jquery'), $version, true);
9042 - wp_enqueue_script('mxchat-admin-testing-js', $plugin_url . 'js/admin-testing-tab.js', array('jquery', 'mxchat-chat-js'), $version, true);
9043 -
9044 - // Allow add-ons to enqueue their public CSS/JS for the testing chatbot
9045 - do_action('mxchat_enqueue_testing_tab_assets');
9046 -
9047 - // Localize mxchatChat for the chatbot JS (same settings as frontend)
9048 - $options = get_option('mxchat_options', array());
9049 - $prompts_options = get_option('mxchat_prompts_options', array());
9050 - $theme_options = get_option('mxchat_theme_options', array());
9051 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
9052 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
9053 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
9054 -
9055 - wp_localize_script('mxchat-chat-js', 'mxchatChat', array(
9056 - 'ajax_url' => admin_url('admin-ajax.php'),
9057 - 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
9058 - 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
9059 - 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
9060 - 'contextual_awareness_toggle' => isset($options['contextual_awareness_toggle']) ? $options['contextual_awareness_toggle'] : 'off',
9061 - 'link_target_toggle' => $options['link_target_toggle'] ?? 'off',
9062 - 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
9063 - 'complianz_toggle' => isset($options['complianz_toggle']) && $options['complianz_toggle'] === 'on',
9064 - 'user_message_bg_color' => $options['user_message_bg_color'] ?? '#fff',
9065 - 'user_message_font_color' => $options['user_message_font_color'] ?? '#212121',
9066 - 'bot_message_bg_color' => $options['bot_message_bg_color'] ?? '#212121',
9067 - 'bot_message_font_color' => $options['bot_message_font_color'] ?? '#fff',
9068 - 'top_bar_bg_color' => $options['top_bar_bg_color'] ?? '#212121',
9069 - 'send_button_font_color' => $options['send_button_font_color'] ?? '#212121',
9070 - 'close_button_color' => $options['close_button_color'] ?? '#fff',
9071 - 'chatbot_background_color' => $options['chatbot_background_color'] ?? '#212121',
9072 - 'chatbot_bg_color' => $options['chatbot_bg_color'] ?? '#fff',
9073 - 'icon_color' => $options['icon_color'] ?? '#fff',
9074 - 'chat_input_font_color' => $options['chat_input_font_color'] ?? '#212121',
9075 - 'chat_persistence_toggle' => 'off', // Always off for testing chatbot
9076 - 'appendWidgetToBody' => 'off',
9077 - 'live_agent_message_bg_color' => $options['live_agent_message_bg_color'] ?? '#ffffff',
9078 - 'live_agent_message_font_color' => $options['live_agent_message_font_color'] ?? '#333333',
9079 - 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
9080 - 'mode_indicator_bg_color' => $options['mode_indicator_bg_color'] ?? '#767676',
9081 - 'mode_indicator_font_color' => $options['mode_indicator_font_color'] ?? '#ffffff',
9082 - 'toolbar_icon_color' => $options['toolbar_icon_color'] ?? '#212121',
9083 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
9084 - 'email_collection_enabled' => 'off', // No email collection in testing
9085 - 'initial_email_state' => null,
9086 - 'skip_email_check' => true,
9087 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
9088 - 'skip_inline_colors' => $skip_inline_colors,
9089 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array()
9090 - ));
9091 -
9092 - // Localize testing tab script data
9093 - wp_localize_script('mxchat-admin-testing-js', 'mxchatAdminTestData', array(
9094 - 'ajaxUrl' => admin_url('admin-ajax.php'),
9095 - 'nonce' => wp_create_nonce('mxchat_test_nonce'),
9096 - 'isAdmin' => true,
9097 - 'testingEnabled' => true
9098 - ));
9099 -
9100 - // Add testing enabled flag for the chatbot to return debug data
9101 - add_action('admin_footer', function() {
9102 - echo '<script>window.mxchatTestingEnabled = true;</script>';
9103 - });
9104 -
9105 7034 break;
9106 7035 }
9107 7036 }
9108 7037 private function localize_admin_scripts($current_page) {
@@ -9117,12 +7046,8 @@
9117 7046 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
9118 7047 'actions_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9119 7048 'add_intent_nonce' => wp_create_nonce('mxchat_add_intent_nonce'),
9120 7049 'edit_intent_nonce' => wp_create_nonce('mxchat_edit_intent'),
9121 - 'add_phrase_nonce' => wp_create_nonce('mxchat_add_phrase_nonce'),
9122 - 'delete_phrase_nonce' => wp_create_nonce('mxchat_delete_phrase_nonce'),
9123 - 'get_phrases_nonce' => wp_create_nonce('mxchat_get_phrases_nonce'),
9124 - 'delete_legacy_nonce' => wp_create_nonce('mxchat_delete_legacy_nonce'),
9125 7050 'toggle_action_nonce' => wp_create_nonce('mxchat_actions_nonce'),
9126 7051 'fetch_openrouter_models_nonce' => wp_create_nonce('mxchat_fetch_openrouter_models'),
9127 7052 'is_activated' => $this->is_activated ? '1' : '0',
9128 7053 'status_refresh_interval' => 5000,
@@ -9132,16 +7057,8 @@
9132 7057
9133 7058 // Localize main admin script with base data
9134 7059 wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data);
9135 7060
9136 - // Canonical chat-model catalog for the modal picker grid
9137 - // (plan-d14e89). Adding a model in class-mxchat-model-catalog.php
9138 - // automatically appears here.
9139 - if (!class_exists('MxChat_Model_Catalog')) {
9140 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9141 - }
9142 - wp_localize_script('mxchat-admin-js', 'mxchatChatModelCatalog', MxChat_Model_Catalog::js_picker_shape());
9143 -
9144 7061 // Page-specific localizations
9145 7062 $this->localize_page_specific_scripts($current_page);
9146 7063 }
9147 7064 private function localize_page_specific_scripts($current_page) {
@@ -9153,22 +7070,17 @@
9153 7070 'nonce' => wp_create_nonce('mxchat_status_nonce')
9154 7071 ));
9155 7072
9156 7073 // Content selector localization
9157 - $mxchat_options_for_selector = get_option('mxchat_options', array());
9158 - $acf_pdf_extract_default = !empty($mxchat_options_for_selector['acf_pdf_extract_default']);
9159 7074 wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array(
9160 7075 'ajaxurl' => admin_url('admin-ajax.php'),
9161 7076 'nonce' => wp_create_nonce('mxchat_content_selector_nonce'),
9162 - 'acfPdfExtractDefault' => $acf_pdf_extract_default ? 1 : 0,
9163 7077 'i18n' => array(
9164 7078 'searchPlaceholder' => __('Search posts and pages...', 'mxchat'),
9165 7079 'selectAll' => __('Select All', 'mxchat'),
9166 7080 'process' => __('Process Selected', 'mxchat'),
9167 7081 'cancel' => __('Cancel', 'mxchat'),
9168 - 'noResults' => __('No content found.', 'mxchat'),
9169 - 'extractingPdfs' => __('extracting %d PDF(s)...', 'mxchat'),
9170 - 'pdfExtractedSuffix' => __(' (%d PDF(s) extracted)', 'mxchat')
7082 + 'noResults' => __('No content found.', 'mxchat')
9171 7083 )
9172 7084 ));
9173 7085
9174 7086 wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array(
@@ -9309,13 +7221,8 @@
9309 7221 $new_input['rag_sources_limit'] = absint($input['rag_sources_limit']); // Ensure it's an integer
9310 7222 $new_input['rag_sources_limit'] = min(max($new_input['rag_sources_limit'], 3), 10); // Enforce range 3-10
9311 7223 }
9312 7224
9313 - if (isset($input['rag_chunks_limit'])) {
9314 - $new_input['rag_chunks_limit'] = absint($input['rag_chunks_limit']); // Ensure it's an integer
9315 - $new_input['rag_chunks_limit'] = min(max($new_input['rag_chunks_limit'], 8), 20); // Enforce range 8-20
9316 - }
9317 -
9318 7225 if (isset($input['xai_api_key'])) {
9319 7226 $new_input['xai_api_key'] = sanitize_text_field($input['xai_api_key']);
9320 7227 }
9321 7228
@@ -9398,48 +7305,16 @@
9398 7305 if (isset($input['contextual_awareness_toggle'])) {
9399 7306 $new_input['contextual_awareness_toggle'] = $input['contextual_awareness_toggle'] === 'on' ? 'on' : 'off';
9400 7307 }
9401 7308
7309 + if (isset($input['show_frontend_debugger'])) {
7310 + $new_input['show_frontend_debugger'] = $input['show_frontend_debugger'] === 'on' ? 'on' : 'off';
7311 +}
7312 +
9402 7313 if (isset($input['citation_links_toggle'])) {
9403 7314 $new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off';
9404 7315 }
9405 7316
9406 - // Satisfaction rating prompt — defaults ON when unchecked (so first save
9407 - // doesn't accidentally disable it). The form posts 'on' when checked;
9408 - // unchecked checkboxes don't post a value at all, so we infer 'off' only
9409 - // when the autosave/PHP request explicitly clears it via empty string.
9410 - if (array_key_exists('satisfaction_rating_enabled', $input)) {
9411 - $new_input['satisfaction_rating_enabled'] = $input['satisfaction_rating_enabled'] === 'on' ? 'on' : 'off';
9412 - }
9413 -
9414 - // Satisfaction rating customization (plan-141a12). Idle is clamped 5-600;
9415 - // the 4 text strings are sanitized + capped server-side. Blank values are
9416 - // preserved so the integrator falls back to translated defaults.
9417 - if (array_key_exists('satisfaction_rating_idle_seconds', $input)) {
9418 - $new_input['satisfaction_rating_idle_seconds'] = max(5, min(600, intval($input['satisfaction_rating_idle_seconds'])));
9419 - }
9420 -
9421 - // Max input length in characters (plan a3fae2 part C). 0 = unlimited (default,
9422 - // preserves current behavior). Clamp to a sane ceiling so a typo can't lock out
9423 - // all input. REQUIRED here — mxchat_sanitize() rebuilds the array from a whitelist,
9424 - // so without this isset-handler the key is stripped on the next save of ANY field
9425 - // (the 2c02ea global-rate-limit footgun).
9426 - if (array_key_exists('max_input_length', $input)) {
9427 - $new_input['max_input_length'] = max(0, min(100000, intval($input['max_input_length'])));
9428 - }
9429 - if (array_key_exists('satisfaction_rating_question', $input)) {
9430 - $new_input['satisfaction_rating_question'] = mb_substr(sanitize_text_field($input['satisfaction_rating_question']), 0, 200);
9431 - }
9432 - if (array_key_exists('satisfaction_rating_thanks', $input)) {
9433 - $new_input['satisfaction_rating_thanks'] = mb_substr(sanitize_text_field($input['satisfaction_rating_thanks']), 0, 300);
9434 - }
9435 - if (array_key_exists('satisfaction_rating_placeholder', $input)) {
9436 - $new_input['satisfaction_rating_placeholder'] = mb_substr(sanitize_text_field($input['satisfaction_rating_placeholder']), 0, 200);
9437 - }
9438 - if (array_key_exists('satisfaction_rating_saved', $input)) {
9439 - $new_input['satisfaction_rating_saved'] = mb_substr(sanitize_text_field($input['satisfaction_rating_saved']), 0, 200);
9440 - }
9441 -
9442 7317 if (isset($input['top_bar_title'])) {
9443 7318 $new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']);
9444 7319 }
9445 7320
@@ -9491,13 +7366,9 @@
9491 7366
9492 7367 // Sanitize limit
9493 7368 if (isset($settings['limit'])) {
9494 7369 $limit = sanitize_text_field($settings['limit']);
9495 - // Accept presets, 'unlimited', the '__custom__' sentinel, OR any positive
9496 - // integer (custom value) — mirrors the global branch (plan-2c02ea). Without
9497 - // the custom path the per-role custom input was dropped and reset to the role
9498 - // default on every save (plan-7e23e7).
9499 - if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
7370 + if (in_array($limit, $allowed_limits, true)) {
9500 7371 $new_input['rate_limits'][$role_id]['limit'] = $limit;
9501 7372 } else {
9502 7373 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
9503 7374 }
@@ -9502,13 +7373,8 @@
9502 7373 $new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default
9503 7374 }
9504 7375 }
9505 7376
9506 - // Preserve the per-role custom value (mirrors the global branch's limit_custom).
9507 - if (isset($settings['limit_custom'])) {
9508 - $new_input['rate_limits'][$role_id]['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $settings['limit_custom']);
9509 - }
9510 -
9511 7377 // Sanitize timeframe
9512 7378 if (isset($settings['timeframe'])) {
9513 7379 $timeframe = sanitize_text_field($settings['timeframe']);
9514 7380 if (in_array($timeframe, $allowed_timeframes, true)) {
@@ -9524,46 +7390,8 @@
9524 7390 }
9525 7391 }
9526 7392 }
9527 7393
9528 -// Handle the whole-chatbot global rate limit (plan-mxchat-20260603-2c02ea).
9529 -// Mirrors the per-role block above, adapted to the single global shape. Without
9530 -// this branch the whitelist-rebuild dropped rate_limits_global entirely on every
9531 -// save, so the global cap silently fell back to its 'unlimited' default.
9532 -// MUST accept arbitrary positive integers so the custom-value path (d55f65) is not regressed.
9533 -if (isset($input['rate_limits_global']) && is_array($input['rate_limits_global'])) {
9534 - $g = $input['rate_limits_global'];
9535 - $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited');
9536 - $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly');
9537 - $global_out = array();
9538 -
9539 - if (isset($g['limit'])) {
9540 - $limit = sanitize_text_field($g['limit']);
9541 - // Accept presets, 'unlimited', the '__custom__' sentinel (resolved by the
9542 - // autosave handler / renderer), OR any positive integer (custom value).
9543 - if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) {
9544 - $global_out['limit'] = $limit;
9545 - } else {
9546 - $global_out['limit'] = 'unlimited';
9547 - }
9548 - }
9549 -
9550 - if (isset($g['limit_custom'])) {
9551 - $global_out['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $g['limit_custom']);
9552 - }
9553 -
9554 - if (isset($g['timeframe'])) {
9555 - $timeframe = sanitize_text_field($g['timeframe']);
9556 - $global_out['timeframe'] = in_array($timeframe, $allowed_timeframes, true) ? $timeframe : 'daily';
9557 - }
9558 -
9559 - if (isset($g['message'])) {
9560 - $global_out['message'] = sanitize_textarea_field($g['message']);
9561 - }
9562 -
9563 - $new_input['rate_limits_global'] = $global_out;
9564 -}
9565 -
9566 7394 if (isset($input['pre_chat_message'])) {
9567 7395 $new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']);
9568 7396 }
9569 7397
@@ -9572,14 +7400,15 @@
9572 7400 }
9573 7401
9574 7402 // Add to your sanitize function
9575 7403 if (isset($input['embedding_model'])) {
9576 - // Catalog refactor (plan-d14e89): allowlist derived from the canonical
9577 - // catalog in includes/class-mxchat-model-catalog.php.
9578 - if (!class_exists('MxChat_Model_Catalog')) {
9579 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9580 - }
9581 - $allowed_models = MxChat_Model_Catalog::embedding_model_ids();
7404 + $allowed_models = array(
7405 + 'text-embedding-ada-002',
7406 + 'text-embedding-3-small',
7407 + 'text-embedding-3-large',
7408 + 'voyage-3-large',
7409 + 'gemini-embedding-001'
7410 + );
9582 7411 if (in_array($input['embedding_model'], $allowed_models)) {
9583 7412 $new_input['embedding_model'] = sanitize_text_field($input['embedding_model']);
9584 7413 }
9585 7414 }
@@ -9587,12 +7416,42 @@
9587 7416 if (isset($input['model'])) {
9588 7417 if ($input['model'] === 'openrouter') {
9589 7418 $new_input['model'] = 'openrouter';
9590 7419 } else {
9591 - if (!class_exists('MxChat_Model_Catalog')) {
9592 - require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php';
9593 - }
9594 - $allowed_models = MxChat_Model_Catalog::chat_model_ids();
7420 + $allowed_models = array(
7421 + 'gemini-3-pro-preview',
7422 + 'gemini-3-flash-preview',
7423 + 'gemini-2.5-pro',
7424 + 'gemini-2.5-flash',
7425 + 'gemini-2.5-flash-lite',
7426 + 'gemini-2.0-flash',
7427 + 'gemini-2.0-flash-lite',
7428 + 'gemini-1.5-pro',
7429 + 'gemini-1.5-flash',
7430 + 'grok-4-0709',
7431 + 'grok-4-1-fast-reasoning',
7432 + 'grok-4-1-fast-non-reasoning',
7433 + 'grok-3-beta',
7434 + 'grok-3-fast-beta',
7435 + 'grok-3-mini-beta',
7436 + 'grok-3-mini-fast-beta',
7437 + 'grok-2',
7438 + 'deepseek-chat',
7439 + 'claude-opus-4-6',
7440 + 'claude-sonnet-4-6',
7441 + 'claude-opus-4-5',
7442 + 'claude-sonnet-4-5-20250929',
7443 + 'claude-opus-4-1-20250805',
7444 + 'claude-haiku-4-5-20251001',
7445 + 'claude-opus-4-20250514',
7446 + 'claude-sonnet-4-20250514',
7447 + 'gpt-5.2',
7448 + 'gpt-5.1-chat-latest',
7449 + 'gpt-5.1-2025-11-13',
7450 + 'gpt-5',
7451 + 'gpt-5-mini',
7452 + 'gpt-5-nano',
7453 + );
9595 7454
9596 7455 if (in_array($input['model'], $allowed_models)) {
9597 7456 $new_input['model'] = sanitize_text_field($input['model']);
9598 7457 } else {
@@ -9615,37 +7474,10 @@
9615 7474 if (isset($input['openrouter_api_key'])) {
9616 7475 $new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']);
9617 7476 }
9618 7477
9619 - // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, Azure OpenAI, etc.
9620 - if (isset($input['custom_provider_base_url'])) {
9621 - $new_input['custom_provider_base_url'] = esc_url_raw(rtrim(trim((string) $input['custom_provider_base_url']), '/'));
9622 - }
9623 - if (isset($input['custom_provider_api_key'])) {
9624 - $new_input['custom_provider_api_key'] = sanitize_text_field($input['custom_provider_api_key']);
9625 - }
9626 - if (isset($input['custom_provider_model'])) {
9627 - $new_input['custom_provider_model'] = sanitize_text_field($input['custom_provider_model']);
9628 - }
9629 - if (isset($input['custom_provider_auth_scheme'])) {
9630 - $scheme = sanitize_text_field($input['custom_provider_auth_scheme']);
9631 - $new_input['custom_provider_auth_scheme'] = in_array($scheme, array('bearer', 'api-key'), true) ? $scheme : 'bearer';
9632 - }
9633 - if (isset($input['custom_provider_for_embeddings'])) {
9634 - $new_input['custom_provider_for_embeddings'] = ($input['custom_provider_for_embeddings'] === 'on') ? 'on' : 'off';
9635 - }
9636 - if (isset($input['custom_provider_for_images'])) {
9637 - $new_input['custom_provider_for_images'] = ($input['custom_provider_for_images'] === 'on') ? 'on' : 'off';
9638 - }
9639 - if (isset($input['custom_provider_embedding_model'])) {
9640 - $new_input['custom_provider_embedding_model'] = sanitize_text_field($input['custom_provider_embedding_model']);
9641 - }
9642 - if (isset($input['custom_provider_api_version'])) {
9643 - $new_input['custom_provider_api_version'] = sanitize_text_field($input['custom_provider_api_version']);
9644 - }
9645 7478
9646 7479
9647 -
9648 7480 if (isset($input['woocommerce_consumer_key'])) {
9649 7481 $new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']);
9650 7482 }
9651 7483
@@ -9667,25 +7499,8 @@
9667 7499 if (isset($input['chat_persistence_toggle'])) {
9668 7500 $new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off';
9669 7501 }
9670 7502
9671 - // No else clause: an absent key stays absent, so the front-end default ('on') applies
9672 - // and the rebuild never strips a saved 'off' (autosave passes the full options array back through here).
9673 - if (isset($input['print_button_enabled'])) {
9674 - $new_input['print_button_enabled'] = $input['print_button_enabled'] === 'on' ? 'on' : 'off';
9675 - }
9676 -
9677 - // plan ac2e81 — "Start new chat" toggle (default OFF) + editable label.
9678 - // No else on the toggle: an absent key stays absent so the front-end '?? off'
9679 - // default applies, and the full-array autosave rebuild preserves a saved value.
9680 - if (isset($input['reset_chat_enabled'])) {
9681 - $new_input['reset_chat_enabled'] = $input['reset_chat_enabled'] === 'on' ? 'on' : 'off';
9682 - }
9683 -
9684 - if (isset($input['reset_chat_label'])) {
9685 - $new_input['reset_chat_label'] = sanitize_text_field($input['reset_chat_label']);
9686 - }
9687 -
9688 7503 if (isset($input['popular_question_1'])) {
9689 7504 $new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']);
9690 7505 }
9691 7506
@@ -9844,12 +7659,8 @@
9844 7659 }
9845 7660 if (isset($input['content_image_model'])) {
9846 7661 $new_input['content_image_model'] = sanitize_text_field($input['content_image_model']);
9847 7662 }
9848 - if (isset($input['content_image_quality'])) {
9849 - $q = sanitize_text_field($input['content_image_quality']);
9850 - $new_input['content_image_quality'] = in_array($q, array('auto', 'low', 'medium', 'high'), true) ? $q : 'auto';
9851 - }
9852 7663 if (isset($input['content_enable_images'])) {
9853 7664 $new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off';
9854 7665 }
9855 7666 if (isset($input['content_use_placeholders'])) {
@@ -9860,11 +7671,8 @@
9860 7671 }
9861 7672 if (isset($input['content_tool_use'])) {
9862 7673 $new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off';
9863 7674 }
9864 - if (isset($input['content_image_count'])) {
9865 - $new_input['content_image_count'] = (string) max(1, min(5, (int) $input['content_image_count']));
9866 - }
9867 7675
9868 7676 // SEO Optimize toggle fields
9869 7677 foreach (array('seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $seo_key) {
9870 7678 if (isset($input[$seo_key])) {
@@ -9874,9 +7682,9 @@
9874 7682
9875 7683 // Preserve content generator settings when saving from main settings page
9876 7684 // (where content fields are not in the form submission)
9877 7685 $existing = get_option('mxchat_options', array());
9878 - foreach (array('content_model', 'content_image_model', 'content_image_quality', 'content_image_count', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $key) {
7686 + foreach (array('content_model', 'content_image_model', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img') as $key) {
9879 7687 if (!isset($new_input[$key]) && isset($existing[$key])) {
9880 7688 $new_input[$key] = $existing[$key];
9881 7689 }
9882 7690 }
@@ -10100,65 +7908,73 @@
10100 7908 //error_log('DEBUG: Permission check failed');
10101 7909 wp_die(__('You do not have sufficient permissions to delete all prompts.', 'mxchat'));
10102 7910 }
10103 7911
10104 - // Get bot_id and content type filter from POST data
7912 + // Get bot_id from POST data
10105 7913 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field($_POST['bot_id']) : 'default';
10106 - $content_type_filter = isset($_POST['content_type_filter']) ? sanitize_text_field($_POST['content_type_filter']) : '';
10107 -
7914 + //error_log('DEBUG: Bot ID from POST: ' . $bot_id);
7915 +
10108 7916 $success = true;
10109 7917 $error_messages = array();
10110 -
7918 +
10111 7919 // Get bot-specific Pinecone configuration
10112 7920 $pinecone_manager = MxChat_Pinecone_Manager::get_instance();
10113 7921 $pinecone_options = $pinecone_manager->mxchat_get_bot_pinecone_options($bot_id);
10114 -
7922 +
7923 + //error_log('DEBUG: Pinecone options retrieved:');
7924 + //error_log(' - Use Pinecone: ' . ($pinecone_options['mxchat_use_pinecone'] ?? 'NOT SET'));
7925 + //error_log(' - API Key: ' . (empty($pinecone_options['mxchat_pinecone_api_key']) ? 'EMPTY' : 'SET'));
7926 + //error_log(' - Host: ' . ($pinecone_options['mxchat_pinecone_host'] ?? 'NOT SET'));
7927 +
10115 7928 $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
10116 -
7929 +
10117 7930 if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
10118 - // Delete from Pinecone (with optional content type filter)
10119 - $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter);
10120 -
7931 + //error_log('[MXCHAT-DELETE] Pinecone is enabled for bot ' . $bot_id . ' - deleting all from Pinecone');
7932 +
7933 + // Delete from bot-specific Pinecone index
7934 + $result = $pinecone_manager->mxchat_delete_all_from_pinecone($pinecone_options);
7935 +
7936 + //error_log('DEBUG: Delete all result: ' . ($result['success'] ? 'SUCCESS' : 'FAILED'));
10121 7937 if (!$result['success']) {
7938 + //error_log('DEBUG: Delete all error: ' . $result['message']);
7939 + }
7940 +
7941 + if (!$result['success']) {
10122 7942 $success = false;
10123 7943 $error_messages[] = $result['message'];
10124 7944 }
10125 -
7945 +
7946 + // No cache clearing needed since we removed caching
7947 +
10126 7948 } else {
7949 + //error_log('[MXCHAT-DELETE] Pinecone not enabled for bot ' . $bot_id . ' - deleting from WordPress database');
7950 +
10127 7951 // Delete from WordPress database
10128 7952 global $wpdb;
10129 7953 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
10130 -
10131 - // Build WHERE conditions
10132 - $where_clauses = array();
10133 - $where_values = array();
10134 -
10135 - // Bot filter
7954 +
7955 + // If multi-bot is active and we have a specific bot, delete only that bot's content
10136 7956 if ($bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager')) {
10137 - $where_clauses[] = 'bot_id = %s';
10138 - $where_values[] = $bot_id;
10139 - }
10140 -
10141 - // Content type filter
10142 - if (!empty($content_type_filter)) {
10143 - $where_clauses[] = 'content_type = %s';
10144 - $where_values[] = $content_type_filter;
10145 - }
10146 -
10147 - if (!empty($where_clauses)) {
10148 - $where_sql = implode(' AND ', $where_clauses);
10149 - $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE {$where_sql}", $where_values));
7957 + $result = $wpdb->delete(
7958 + $table_name,
7959 + array('bot_id' => $bot_id),
7960 + array('%s')
7961 + );
10150 7962 } else {
10151 - // No filters — delete all
7963 + // Delete all content for default bot
10152 7964 $result = $wpdb->query("DELETE FROM {$table_name}");
10153 7965 }
10154 -
7966 +
10155 7967 if ($result === false) {
10156 7968 $success = false;
10157 7969 $error_messages[] = 'Failed to delete from WordPress database';
10158 7970 }
10159 7971 }
7972 +
7973 + // No cache clearing needed since we removed caching
10160 7974
7975 + //error_log('=== DELETE ALL DEBUG END ===');
7976 +
10161 7977 // Redirect back with a success message and bot_id
10162 7978 $redirect_url = add_query_arg(array(
10163 7979 'page' => 'mxchat-prompts',
10164 7980 'bot_id' => $bot_id,
@@ -10463,9 +8279,8 @@
10463 8279 if ($embedding_dimensions !== 1536) {
10464 8280 //error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions);
10465 8281 }
10466 8282
10467 - MxChat_Utils::stamp_active_embedding_model($selected_model);
10468 8283 return $response_data['embedding']['values'];
10469 8284 } else {
10470 8285 //error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response');
10471 8286 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));
@@ -10491,9 +8306,8 @@
10491 8306 ($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) {
10492 8307 //error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions');
10493 8308 }
10494 8309
10495 - MxChat_Utils::stamp_active_embedding_model($selected_model);
10496 8310 return $response_data['data'][0]['embedding'];
10497 8311 } else {
10498 8312 //error_log('[MXCHAT-EMBED] Error: No embedding found in response');
10499 8313 //error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data)));