|
@@ -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')); |
|
@@ -107,146 +90,10 @@ |
|
107
|
90
|
|
|
108
|
91
|
// Translation handlers |
|
109
|
92
|
add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages')); |
|
110
|
93
|
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
|
94
|
} |
|
117
|
95
|
|
|
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
|
96
|
private function is_license_active() { |
|
250
|
97
|
$license_status = get_option('mxchat_license_status', 'inactive'); |
|
251
|
98
|
return ($license_status === 'active'); |
|
252
|
99
|
} |
|
@@ -298,14 +145,8 @@ |
|
298
|
145
|
'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude' |
|
299
|
146
|
'post_type_visibility_list' => array(), // Array of post type slugs |
|
300
|
147
|
'contextual_awareness_toggle' => 'off', |
|
301
|
148
|
'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' => '', |
|
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')) { |
|
@@ -3014,11 +1901,8 @@ |
|
3014
|
1901
|
/** |
|
3015
|
1902
|
* Translate text using Google Gemini API |
|
3016
|
1903
|
*/ |
|
3017
|
1904
|
private function translate_with_gemini($api_key, $model, $system_prompt, $text) { |
|
3018
|
|
- if ($model === 'gemini-3-pro-preview') { |
|
3019
|
|
- $model = 'gemini-3.1-pro-preview'; |
|
3020
|
|
- } |
|
3021
|
1905
|
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key; |
|
3022
|
1906
|
|
|
3023
|
1907
|
$response = wp_remote_post($url, [ |
|
3024
|
1908
|
'timeout' => 60, |
|
@@ -3108,20 +1992,9 @@ |
|
3108
|
1992
|
$page = isset($_POST['page']) ? absint($_POST['page']) : 1; |
|
3109
|
1993
|
$per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 50; |
|
3110
|
1994
|
$offset = ($page - 1) * $per_page; |
|
3111
|
1995
|
$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : ''; |
|
3112
|
|
- $sort_raw = isset($_POST['sort_order']) ? sanitize_key($_POST['sort_order']) : 'desc'; |
|
3113
|
|
- $allowed_sorts = array('asc', 'desc', 'rating_positive', 'rating_negative'); |
|
3114
|
|
- if (!in_array($sort_raw, $allowed_sorts, true)) { $sort_raw = 'desc'; } |
|
3115
|
|
- $sort_order = ($sort_raw === 'asc') ? 'ASC' : 'DESC'; |
|
3116
|
|
- $ratings_table = $wpdb->prefix . 'mxchat_session_ratings'; |
|
3117
|
|
- $ratings_join = ''; |
|
3118
|
|
- $rating_table_exists = ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $ratings_table)) === $ratings_table); |
|
3119
|
|
- if ($rating_table_exists && ($sort_raw === 'rating_positive' || $sort_raw === 'rating_negative')) { |
|
3120
|
|
- // We sort by rating then recency. Wrap rating_value so NULL sorts last. |
|
3121
|
|
- $rating_dir = ($sort_raw === 'rating_positive') ? 'DESC' : 'ASC'; |
|
3122
|
|
- $ratings_join = " LEFT JOIN {$ratings_table} r ON r.session_id = t.session_id "; |
|
3123
|
|
- } |
|
1996
|
+ $sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] === 'asc' ? 'ASC' : 'DESC'; |
|
3124
|
1997
|
|
|
3125
|
1998
|
// Build search condition |
|
3126
|
1999
|
$search_condition = ''; |
|
3127
|
2000
|
$search_params = []; |
|
@@ -3141,24 +2014,18 @@ |
|
3141
|
2014
|
? $wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table_name} {$search_condition}", $search_params) |
|
3142
|
2015
|
: "SELECT COUNT(DISTINCT session_id) FROM {$table_name}"; |
|
3143
|
2016
|
$total_sessions = (int) $wpdb->get_var($count_query); |
|
3144
|
2017
|
|
|
3145
|
|
- // Get session IDs for current page. Default sort is recency; rating sorts join the ratings table |
|
3146
|
|
- // and order by rating value (NULLs last) with recency as tiebreaker. |
|
3147
|
|
- if ($ratings_join) { |
|
3148
|
|
- $order_by = "ORDER BY (r.rating_value IS NULL), r.rating_value {$rating_dir}, MAX(t.timestamp) DESC"; |
|
3149
|
|
- } else { |
|
3150
|
|
- $order_by = "ORDER BY MAX(t.timestamp) {$sort_order}"; |
|
3151
|
|
- } |
|
2018
|
+ // Get session IDs for current page (sorted by newest or oldest) |
|
3152
|
2019
|
$session_query = !empty($search) |
|
3153
|
2020
|
? $wpdb->prepare( |
|
3154
|
|
- "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join} {$search_condition} |
|
3155
|
|
- 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", |
|
3156
|
2023
|
array_merge($search_params, [$per_page, $offset]) |
|
3157
|
2024
|
) |
|
3158
|
2025
|
: $wpdb->prepare( |
|
3159
|
|
- "SELECT DISTINCT t.session_id FROM {$table_name} t {$ratings_join} |
|
3160
|
|
- 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", |
|
3161
|
2028
|
$per_page, $offset |
|
3162
|
2029
|
); |
|
3163
|
2030
|
$session_ids = $wpdb->get_col($session_query); |
|
3164
|
2031
|
|
|
@@ -3180,24 +2047,8 @@ |
|
3180
|
2047
|
// Check for optional columns/tables |
|
3181
|
2048
|
$url_table_exists = $wpdb->get_var("SHOW TABLES LIKE '$url_clicks_table'") === $url_clicks_table; |
|
3182
|
2049
|
$originating_columns_exist = !empty($wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'")); |
|
3183
|
2050
|
|
|
3184
|
|
- // Batch-fetch session ratings for this page (plan-a5b006). |
|
3185
|
|
- $ratings_map = array(); |
|
3186
|
|
- if ($rating_table_exists && !empty($session_ids)) { |
|
3187
|
|
- $placeholders = implode(',', array_fill(0, count($session_ids), '%s')); |
|
3188
|
|
- $rating_rows = $wpdb->get_results($wpdb->prepare( |
|
3189
|
|
- "SELECT session_id, rating_value, rating_feedback FROM {$ratings_table} WHERE session_id IN ($placeholders)", |
|
3190
|
|
- $session_ids |
|
3191
|
|
- )); |
|
3192
|
|
- foreach ($rating_rows as $row) { |
|
3193
|
|
- $ratings_map[$row->session_id] = array( |
|
3194
|
|
- 'value' => (int) $row->rating_value, |
|
3195
|
|
- 'feedback' => (string) $row->rating_feedback, |
|
3196
|
|
- ); |
|
3197
|
|
- } |
|
3198
|
|
- } |
|
3199
|
|
- |
|
3200
|
2051
|
// Build session list data |
|
3201
|
2052
|
$sessions = []; |
|
3202
|
2053
|
foreach ($session_ids as $session_id) { |
|
3203
|
2054
|
// Get session metadata |
|
@@ -3254,12 +2105,8 @@ |
|
3254
|
2105
|
$parts = explode(' ', $display_name); |
|
3255
|
2106
|
$initials = strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1)); |
|
3256
|
2107
|
} |
|
3257
|
2108
|
|
|
3258
|
|
- $rating_entry = isset($ratings_map[$session_id]) ? $ratings_map[$session_id] : null; |
|
3259
|
|
- $rating_value = $rating_entry ? $rating_entry['value'] : null; |
|
3260
|
|
- $rating_feedback = $rating_entry ? $rating_entry['feedback'] : ''; |
|
3261
|
|
- |
|
3262
|
2109
|
$sessions[] = [ |
|
3263
|
2110
|
'session_id' => $session_id, |
|
3264
|
2111
|
'display_name' => $display_name, |
|
3265
|
2112
|
'display_sub' => $display_sub, |
|
@@ -3266,11 +2113,9 @@ |
|
3266
|
2113
|
'initials' => $initials, |
|
3267
|
2114
|
'preview' => $preview, |
|
3268
|
2115
|
'message_count' => (int) $message_stats->count, |
|
3269
|
2116
|
'time_display' => $time_display, |
|
3270
|
|
- 'timestamp' => $message_stats->latest, |
|
3271
|
|
- 'rating_value' => $rating_value, |
|
3272
|
|
- 'rating_feedback' => $rating_feedback, |
|
2117
|
+ 'timestamp' => $message_stats->latest |
|
3273
|
2118
|
]; |
|
3274
|
2119
|
} |
|
3275
|
2120
|
|
|
3276
|
2121
|
wp_send_json([ |
|
@@ -3408,18 +2253,8 @@ |
|
3408
|
2253
|
|
|
3409
|
2254
|
// First message timestamp for "started" display |
|
3410
|
2255
|
$started = !empty($messages) ? wp_date('M j, Y g:i A', strtotime($messages[0]->timestamp . ' UTC')) : '-'; |
|
3411
|
2256
|
|
|
3412
|
|
- // Pull rating_feedback for this session (if any) so the details drawer can show it. |
|
3413
|
|
- $rating_feedback = ''; |
|
3414
|
|
- $ratings_table_det = $wpdb->prefix . 'mxchat_session_ratings'; |
|
3415
|
|
- if ($wpdb->get_var("SHOW TABLES LIKE '$ratings_table_det'") === $ratings_table_det) { |
|
3416
|
|
- $rating_feedback = (string) $wpdb->get_var($wpdb->prepare( |
|
3417
|
|
- "SELECT rating_feedback FROM {$ratings_table_det} WHERE session_id = %s LIMIT 1", |
|
3418
|
|
- $session_id |
|
3419
|
|
- )); |
|
3420
|
|
- } |
|
3421
|
|
- |
|
3422
|
2257
|
wp_send_json([ |
|
3423
|
2258
|
'success' => true, |
|
3424
|
2259
|
'session_id' => $session_id, |
|
3425
|
2260
|
'user' => [ |
|
@@ -3435,10 +2270,9 @@ |
|
3435
|
2270
|
], |
|
3436
|
2271
|
'clicked_urls' => $clicked_urls, |
|
3437
|
2272
|
'messages' => $formatted_messages, |
|
3438
|
2273
|
'message_count' => count($messages), |
|
3439
|
|
- 'started' => $started, |
|
3440
|
|
- 'rating_feedback' => $rating_feedback |
|
2274
|
+ 'started' => $started |
|
3441
|
2275
|
]); |
|
3442
|
2276
|
wp_die(); |
|
3443
|
2277
|
} |
|
3444
|
2278
|
|
|
@@ -3891,103 +2725,53 @@ |
|
3891
|
2725
|
echo wp_json_encode(['error' => esc_html__('You do not have sufficient permissions.', 'mxchat')]); |
|
3892
|
2726
|
wp_die(); |
|
3893
|
2727
|
} |
|
3894
|
2728
|
check_ajax_referer('mxchat_delete_chat_history', 'security'); |
|
3895
|
|
- |
|
3896
|
|
- if (!isset($_POST['delete_session_ids']) || !is_array($_POST['delete_session_ids'])) { |
|
3897
|
|
- echo wp_json_encode(['error' => esc_html__('No chat sessions selected for deletion.', 'mxchat')]); |
|
3898
|
|
- wp_die(); |
|
3899
|
|
- } |
|
3900
|
|
- |
|
3901
|
2729
|
global $wpdb; |
|
3902
|
2730
|
$table_name = $wpdb->prefix . 'mxchat_chat_transcripts'; |
|
3903
|
|
- $translations_table = $wpdb->prefix . 'mxchat_transcript_translations'; |
|
3904
|
|
- $has_translations = $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table; |
|
3905
|
2731
|
|
|
3906
|
|
- // When true, any lead attached to these sessions is fully wiped (all their sessions, |
|
3907
|
|
- // across the whole table). Default false: the chat rows go away but the lead is |
|
3908
|
|
- // preserved as a separate "chat deleted" lead in the Leads tab. |
|
3909
|
|
- $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'; |
|
3910
|
2735
|
|
|
3911
|
|
- $deleted_count = 0; |
|
3912
|
|
- $preserved_as_deleted_leads = 0; |
|
3913
|
|
- $emails_to_fully_wipe = []; |
|
2736
|
+ foreach ($_POST['delete_session_ids'] as $session_id) { |
|
2737
|
+ $session_id_sanitized = sanitize_text_field($session_id); |
|
3914
|
2738
|
|
|
3915
|
|
- foreach ((array) $_POST['delete_session_ids'] as $session_id) { |
|
3916
|
|
- $session_id_sanitized = sanitize_text_field($session_id); |
|
3917
|
|
- if ($session_id_sanitized === '') { |
|
3918
|
|
- continue; |
|
3919
|
|
- } |
|
2739
|
+ // Clear relevant cache before deletion |
|
2740
|
+ $cache_key = 'chat_session_' . $session_id_sanitized; |
|
2741
|
+ wp_cache_delete($cache_key, 'mxchat_chat_sessions'); |
|
3920
|
2742
|
|
|
3921
|
|
- // Capture the lead info attached to this session *before* we delete the rows. |
|
3922
|
|
- $lead_row = $wpdb->get_row($wpdb->prepare( |
|
3923
|
|
- "SELECT user_email, user_name, MAX(timestamp) AS last_ts |
|
3924
|
|
- FROM {$table_name} |
|
3925
|
|
- WHERE session_id = %s AND user_email IS NOT NULL AND user_email != '' |
|
3926
|
|
- GROUP BY user_email, user_name |
|
3927
|
|
- ORDER BY last_ts DESC LIMIT 1", |
|
3928
|
|
- $session_id_sanitized |
|
3929
|
|
- )); |
|
2743
|
+ // Perform the deletion from the database table |
|
2744
|
+ $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]); |
|
3930
|
2745
|
|
|
3931
|
|
- wp_cache_delete('chat_session_' . $session_id_sanitized, 'mxchat_chat_sessions'); |
|
3932
|
|
- $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
|
+ } |
|
3933
|
2750
|
|
|
3934
|
|
- if ($has_translations) { |
|
3935
|
|
- $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]); |
|
3936
|
|
- } |
|
2751
|
+ // Delete the corresponding option entry from wp_options table |
|
2752
|
+ delete_option("mxchat_history_" . $session_id_sanitized); |
|
3937
|
2753
|
|
|
3938
|
|
- delete_option('mxchat_history_' . $session_id_sanitized); |
|
3939
|
|
- 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); |
|
3940
|
2757
|
|
|
3941
|
|
- if ($lead_row && !empty($lead_row->user_email)) { |
|
3942
|
|
- if ($also_delete_lead) { |
|
3943
|
|
- // Full-wipe requested — queue the email so that all their sessions and |
|
3944
|
|
- // related options get swept below. Also clear this session's pre-chat |
|
3945
|
|
- // capture options (they're no longer meaningful). |
|
3946
|
|
- $emails_to_fully_wipe[strtolower($lead_row->user_email)] = $lead_row->user_email; |
|
3947
|
|
- delete_option('mxchat_email_' . $session_id_sanitized); |
|
3948
|
|
- delete_option('mxchat_name_' . $session_id_sanitized); |
|
3949
|
|
- } else { |
|
3950
|
|
- // Preserve the lead in a "Chat deleted" state via distinct option keys so |
|
3951
|
|
- // they stay out of the orphan bucket (orphan = pre-chat form dropoff). |
|
3952
|
|
- update_option('mxchat_lead_del_email_' . $session_id_sanitized, $lead_row->user_email, false); |
|
3953
|
|
- if (!empty($lead_row->user_name)) { |
|
3954
|
|
- update_option('mxchat_lead_del_name_' . $session_id_sanitized, $lead_row->user_name, false); |
|
3955
|
|
- } |
|
3956
|
|
- if (!empty($lead_row->last_ts)) { |
|
3957
|
|
- update_option('mxchat_lead_del_ts_' . $session_id_sanitized, $lead_row->last_ts, false); |
|
3958
|
|
- } |
|
3959
|
|
- // Clean up pre-chat capture options for this session — chat_deleted supersedes. |
|
3960
|
|
- delete_option('mxchat_email_' . $session_id_sanitized); |
|
3961
|
|
- delete_option('mxchat_name_' . $session_id_sanitized); |
|
3962
|
|
- $preserved_as_deleted_leads++; |
|
3963
|
|
- } |
|
3964
|
|
- } else { |
|
3965
|
|
- // No lead attached — nothing to preserve. Clean up any orphan options anyway. |
|
3966
|
|
- delete_option('mxchat_email_' . $session_id_sanitized); |
|
3967
|
|
- delete_option('mxchat_name_' . $session_id_sanitized); |
|
2758
|
+ $deleted_count++; |
|
3968
|
2759
|
} |
|
3969
|
2760
|
|
|
3970
|
|
- $deleted_count++; |
|
3971
|
|
- } |
|
2761
|
+ // Optionally, clear a general cache if you have one |
|
2762
|
+ wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions'); |
|
3972
|
2763
|
|
|
3973
|
|
- // Opt-in full-lead wipe: sweep every remaining row + every option key (including |
|
3974
|
|
- // chat_deleted preservation) for each affected email. Reuses the same internal |
|
3975
|
|
- // helper as the Leads-tab Delete button for consistency. |
|
3976
|
|
- if (!empty($emails_to_fully_wipe)) { |
|
3977
|
|
- 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')]); |
|
3978
|
2772
|
} |
|
3979
|
2773
|
|
|
3980
|
|
- wp_cache_delete('all_chat_sessions', 'mxchat_chat_sessions'); |
|
3981
|
|
- |
|
3982
|
|
- echo wp_json_encode([ |
|
3983
|
|
- 'success' => sprintf( |
|
3984
|
|
- esc_html__('%d chat session(s) have been deleted.', 'mxchat'), |
|
3985
|
|
- $deleted_count |
|
3986
|
|
- ), |
|
3987
|
|
- 'preserved_as_deleted_leads' => $preserved_as_deleted_leads, |
|
3988
|
|
- 'leads_fully_wiped' => count($emails_to_fully_wipe), |
|
3989
|
|
- ]); |
|
3990
|
2774
|
wp_die(); |
|
3991
|
2775
|
} |
|
3992
|
2776
|
|
|
3993
|
2777
|
/** |
|
@@ -4863,9 +3647,9 @@ |
|
4863
|
3647
|
'intent_label' => $intent_label, |
|
4864
|
3648
|
'phrases' => implode(', ', $phrases_array), |
|
4865
|
3649
|
'embedding_vector' => $serialized_vector, |
|
4866
|
3650
|
'similarity_threshold' => $similarity_threshold, |
|
4867
|
|
- 'enabled_bots' => $enabled_bots_json, // Include enabled_bots in update |
|
3651
|
+ 'enabled_bots' => $enabled_bots_json // Include enabled_bots in update |
|
4868
|
3652
|
), |
|
4869
|
3653
|
array('id' => $intent_id), |
|
4870
|
3654
|
array('%s', '%s', '%s', '%f', '%s'), // Format: string, string, string, float, string |
|
4871
|
3655
|
array('%d') // Where format: integer |
|
@@ -4982,9 +3766,9 @@ |
|
4982
|
3766
|
'phrases' => implode(', ', $phrases_array), |
|
4983
|
3767
|
'embedding_vector' => $serialized_vector, |
|
4984
|
3768
|
'callback_function' => $callback_function, |
|
4985
|
3769
|
'similarity_threshold' => $similarity_threshold, |
|
4986
|
|
- 'enabled_bots' => $enabled_bots_json, // NEW field |
|
3770
|
+ 'enabled_bots' => $enabled_bots_json // NEW field |
|
4987
|
3771
|
]); |
|
4988
|
3772
|
|
|
4989
|
3773
|
if ($result === false) { |
|
4990
|
3774
|
$this->handle_embedding_error(__('Database error: ', 'mxchat') . $wpdb->last_error); |
|
@@ -6056,17 +4840,8 @@ |
|
6056
|
4840
|
'mxchat-api-keys', |
|
6057
|
4841
|
'mxchat_api_keys_section' |
|
6058
|
4842
|
); |
|
6059
|
4843
|
|
|
6060
|
|
- // Custom (OpenAI-compatible) Provider — for Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc. |
|
6061
|
|
- add_settings_field( |
|
6062
|
|
- 'custom_provider', |
|
6063
|
|
- esc_html__('Custom Provider (OpenAI-compatible)', 'mxchat'), |
|
6064
|
|
- array($this, 'custom_provider_callback'), |
|
6065
|
|
- 'mxchat-api-keys', |
|
6066
|
|
- 'mxchat_api_keys_section' |
|
6067
|
|
- ); |
|
6068
|
|
- |
|
6069
|
4844
|
// Loops API Key |
|
6070
|
4845
|
add_settings_field( |
|
6071
|
4846
|
'loops_api_key', |
|
6072
|
4847
|
esc_html__('Loops API Key', 'mxchat'), |
|
@@ -6134,22 +4909,9 @@ |
|
6134
|
4909
|
'mxchat-chatbot', |
|
6135
|
4910
|
'mxchat_chatbot_section' |
|
6136
|
4911
|
); |
|
6137
|
4912
|
|
|
6138
|
|
- // Satisfaction rating toggle (plan-a5b006). |
|
6139
|
4913
|
add_settings_field( |
|
6140
|
|
- 'satisfaction_rating_enabled', |
|
6141
|
|
- esc_html__('Satisfaction Rating Prompt', 'mxchat'), |
|
6142
|
|
- array($this, 'mxchat_satisfaction_rating_toggle_callback'), |
|
6143
|
|
- 'mxchat-chatbot', |
|
6144
|
|
- 'mxchat_chatbot_section' |
|
6145
|
|
- ); |
|
6146
|
|
- |
|
6147
|
|
- // Satisfaction rating customization (plan-141a12, plan-29caac): |
|
6148
|
|
- // the 5 customization fields are now inline-rendered inside |
|
6149
|
|
- // mxchat_satisfaction_rating_toggle_callback's sub-options wrapper. |
|
6150
|
|
- |
|
6151
|
|
- add_settings_field( |
|
6152
|
4914
|
'enable_streaming_toggle', |
|
6153
|
4915
|
esc_html__('Enable Streaming', 'mxchat'), |
|
6154
|
4916
|
array($this, 'enable_streaming_toggle_callback'), |
|
6155
|
4917
|
'mxchat-chatbot', |
|
@@ -6298,16 +5060,8 @@ |
|
6298
|
5060
|
'mxchat_chatbot_section' |
|
6299
|
5061
|
); |
|
6300
|
5062
|
|
|
6301
|
5063
|
add_settings_field( |
|
6302
|
|
- 'print_button_enabled', |
|
6303
|
|
- esc_html__('Show Download Transcript Button', 'mxchat'), |
|
6304
|
|
- array($this, 'mxchat_print_button_toggle_callback'), |
|
6305
|
|
- 'mxchat-chatbot', |
|
6306
|
|
- 'mxchat_chatbot_section' |
|
6307
|
|
- ); |
|
6308
|
|
- |
|
6309
|
|
- add_settings_field( |
|
6310
|
5064
|
'popular_question_1', |
|
6311
|
5065
|
esc_html__('Quick Question 1', 'mxchat'), |
|
6312
|
5066
|
array($this, 'mxchat_popular_question_1_callback'), |
|
6313
|
5067
|
'mxchat-chatbot', |
|
@@ -6697,16 +5451,8 @@ |
|
6697
|
5451
|
'mxchat_transcripts_notification_section' |
|
6698
|
5452
|
); |
|
6699
|
5453
|
|
|
6700
|
5454
|
add_settings_field( |
|
6701
|
|
- 'mxchat_retention_days', |
|
6702
|
|
- esc_html__('Custom Retention (Days)', 'mxchat'), |
|
6703
|
|
- array($this, 'mxchat_retention_days_callback'), |
|
6704
|
|
- 'mxchat-transcripts', |
|
6705
|
|
- 'mxchat_transcripts_notification_section' |
|
6706
|
|
- ); |
|
6707
|
|
- |
|
6708
|
|
- add_settings_field( |
|
6709
|
5455
|
'mxchat_auto_email_transcript', |
|
6710
|
5456
|
esc_html__('Auto-Email Full Transcript', 'mxchat'), |
|
6711
|
5457
|
array($this, 'mxchat_auto_email_transcript_callback'), |
|
6712
|
5458
|
'mxchat-transcripts', |
|
@@ -7006,136 +5752,8 @@ |
|
7006
|
5752
|
echo '<p class="description">' . esc_html__('Required for OpenRouter models. Get your API key from OpenRouter.ai', 'mxchat') . '</p>'; |
|
7007
|
5753
|
echo '</div>'; |
|
7008
|
5754
|
} |
|
7009
|
5755
|
|
|
7010
|
|
-// Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc. |
|
7011
|
|
-public function custom_provider_callback() { |
|
7012
|
|
- $base_url = isset($this->options['custom_provider_base_url']) ? esc_attr($this->options['custom_provider_base_url']) : ''; |
|
7013
|
|
- $api_key = isset($this->options['custom_provider_api_key']) ? esc_attr($this->options['custom_provider_api_key']) : ''; |
|
7014
|
|
- $model_name = isset($this->options['custom_provider_model']) ? esc_attr($this->options['custom_provider_model']) : ''; |
|
7015
|
|
- $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? esc_attr($this->options['custom_provider_auth_scheme']) : 'bearer'; |
|
7016
|
|
- $api_version = isset($this->options['custom_provider_api_version']) ? esc_attr($this->options['custom_provider_api_version']) : ''; |
|
7017
|
|
- $use_embed = !empty($this->options['custom_provider_for_embeddings']) && $this->options['custom_provider_for_embeddings'] === 'on'; |
|
7018
|
|
- $use_images = !empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on'; |
|
7019
|
|
- $embed_model = isset($this->options['custom_provider_embedding_model']) ? esc_attr($this->options['custom_provider_embedding_model']) : ''; |
|
7020
|
|
- $nonce = wp_create_nonce('mxchat_autosave_nonce'); |
|
7021
|
|
- $test_nonce = wp_create_nonce('mxchat_test_custom_provider'); |
|
7022
|
|
- |
|
7023
|
|
- echo '<style> |
|
7024
|
|
- .mxchat-cp { max-width: 680px; } |
|
7025
|
|
- .mxchat-cp .mxchat-cp-intro { margin: 0 0 16px; color: #50575e; font-size: 13px; line-height: 1.5; } |
|
7026
|
|
- .mxchat-cp .mxchat-cp-row { display: block; margin: 0 0 18px; } |
|
7027
|
|
- .mxchat-cp .mxchat-cp-row > label { display: block; font-weight: 600; margin: 0 0 6px; color: #1d2327; font-size: 13px; } |
|
7028
|
|
- .mxchat-cp .mxchat-cp-row > input[type="text"], |
|
7029
|
|
- .mxchat-cp .mxchat-cp-row > input[type="password"], |
|
7030
|
|
- .mxchat-cp .mxchat-cp-row > select { display: block; width: 100%; max-width: 480px; margin: 0; } |
|
7031
|
|
- .mxchat-cp .mxchat-cp-row > .description { display: block; margin: 6px 0 0; color: #646970; font-size: 12px; line-height: 1.5; max-width: 480px; } |
|
7032
|
|
- .mxchat-cp .mxchat-cp-test { margin-top: 4px; padding-top: 14px; border-top: 1px solid #e5e7eb; } |
|
7033
|
|
- .mxchat-cp .mxchat-cp-test .mxchat-cp-test-status { display: inline-block; margin-left: 10px; vertical-align: middle; font-size: 13px; } |
|
7034
|
|
- .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; } |
|
7035
|
|
- .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; } |
|
7036
|
|
- .mxchat-cp .mxchat-cp-azure ol { margin: 0; padding: 0 0 0 18px; color: #50575e; font-size: 12px; line-height: 1.7; } |
|
7037
|
|
- .mxchat-cp .mxchat-cp-azure code { background: #eceefb; padding: 1px 5px; border-radius: 3px; font-size: 11px; } |
|
7038
|
|
- </style>'; |
|
7039
|
|
- |
|
7040
|
|
- echo '<div class="api-key-wrapper mxchat-cp">'; |
|
7041
|
|
- 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>'; |
|
7042
|
|
- |
|
7043
|
|
- // Azure OpenAI quick start — consolidates the 4-field Azure recipe in one scannable callout |
|
7044
|
|
- // so admins can configure Azure without piecing it together from each field's hint. |
|
7045
|
|
- echo '<div class="mxchat-cp-azure">'; |
|
7046
|
|
- echo '<span class="mxchat-cp-azure-title">' . esc_html__('Azure OpenAI quick start', 'mxchat') . '</span>'; |
|
7047
|
|
- echo '<ol>'; |
|
7048
|
|
- echo '<li>' . wp_kses(__('<strong>Base URL</strong> → <code>https://<resource>.openai.azure.com/openai/deployments/<deployment></code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>'; |
|
7049
|
|
- echo '<li>' . wp_kses(__('<strong>API Key</strong> → your Azure OpenAI key (required)', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>'; |
|
7050
|
|
- echo '<li>' . wp_kses(__('<strong>Auth Scheme</strong> → <code>api-key header (Azure OpenAI)</code>', 'mxchat'), array('strong' => array(), 'code' => array())) . '</li>'; |
|
7051
|
|
- 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>'; |
|
7052
|
|
- echo '</ol>'; |
|
7053
|
|
- echo '</div>'; |
|
7054
|
|
- |
|
7055
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7056
|
|
- echo '<label for="custom_provider_base_url">' . esc_html__('Base URL', 'mxchat') . '</label>'; |
|
7057
|
|
- 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 . '" />'; |
|
7058
|
|
- 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>'; |
|
7059
|
|
- echo '</div>'; |
|
7060
|
|
- |
|
7061
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7062
|
|
- echo '<label for="custom_provider_api_key">' . esc_html__('API Key (optional)', 'mxchat') . '</label>'; |
|
7063
|
|
- 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 . '" />'; |
|
7064
|
|
- echo '<p class="description">' . esc_html__('Leave empty for unauthenticated local servers. Required for Azure / vLLM / hosted endpoints.', 'mxchat') . '</p>'; |
|
7065
|
|
- echo '</div>'; |
|
7066
|
|
- |
|
7067
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7068
|
|
- echo '<label for="custom_provider_model">' . esc_html__('Model Name', 'mxchat') . '</label>'; |
|
7069
|
|
- 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 . '" />'; |
|
7070
|
|
- 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>'; |
|
7071
|
|
- echo '</div>'; |
|
7072
|
|
- |
|
7073
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7074
|
|
- echo '<label for="custom_provider_auth_scheme">' . esc_html__('Auth Scheme', 'mxchat') . '</label>'; |
|
7075
|
|
- echo '<select id="custom_provider_auth_scheme" name="custom_provider_auth_scheme" class="mxchat-autosave-field" data-nonce="' . $nonce . '">'; |
|
7076
|
|
- echo '<option value="bearer"' . selected($auth_scheme, 'bearer', false) . '>' . esc_html__('Authorization: Bearer (OpenAI / Ollama / vLLM / LM Studio)', 'mxchat') . '</option>'; |
|
7077
|
|
- echo '<option value="api-key"' . selected($auth_scheme, 'api-key', false) . '>' . esc_html__('api-key header (Azure OpenAI)', 'mxchat') . '</option>'; |
|
7078
|
|
- echo '</select>'; |
|
7079
|
|
- echo '<p class="description">' . esc_html__('Most OpenAI-compatible servers use Bearer. Azure OpenAI uses the api-key header.', 'mxchat') . '</p>'; |
|
7080
|
|
- echo '</div>'; |
|
7081
|
|
- |
|
7082
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7083
|
|
- echo '<label for="custom_provider_api_version">' . esc_html__('API Version (Azure only)', 'mxchat') . '</label>'; |
|
7084
|
|
- 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 . '" />'; |
|
7085
|
|
- 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>'; |
|
7086
|
|
- echo '</div>'; |
|
7087
|
|
- |
|
7088
|
|
- // Extended-use checkboxes — opt-in routing of other dispatcher paths through the custom provider. |
|
7089
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7090
|
|
- 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>'; |
|
7091
|
|
- 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>'; |
|
7092
|
|
- 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>'; |
|
7093
|
|
- 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>'; |
|
7094
|
|
- echo '</div>'; |
|
7095
|
|
- |
|
7096
|
|
- echo '<div class="mxchat-cp-row">'; |
|
7097
|
|
- echo '<label for="custom_provider_embedding_model">' . esc_html__('Custom Embedding Model', 'mxchat') . '</label>'; |
|
7098
|
|
- 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 . '" />'; |
|
7099
|
|
- 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>'; |
|
7100
|
|
- echo '</div>'; |
|
7101
|
|
- |
|
7102
|
|
- echo '<div class="mxchat-cp-test">'; |
|
7103
|
|
- echo '<button type="button" class="button" id="mxchat-test-custom-provider" data-nonce="' . $test_nonce . '">' . esc_html__('Test Connection', 'mxchat') . '</button>'; |
|
7104
|
|
- echo '<span id="mxchat-test-custom-provider-result" class="mxchat-cp-test-status"></span>'; |
|
7105
|
|
- echo '</div>'; |
|
7106
|
|
- |
|
7107
|
|
- echo '<script>(function(){ |
|
7108
|
|
- var btn = document.getElementById("mxchat-test-custom-provider"); |
|
7109
|
|
- if (!btn || btn._wired) { return; } btn._wired = true; |
|
7110
|
|
- btn.addEventListener("click", function(){ |
|
7111
|
|
- var out = document.getElementById("mxchat-test-custom-provider-result"); |
|
7112
|
|
- out.textContent = "' . esc_js(__('Testing...', 'mxchat')) . '"; |
|
7113
|
|
- out.style.color = "#646970"; |
|
7114
|
|
- var fd = new FormData(); |
|
7115
|
|
- fd.append("action", "mxchat_test_custom_provider"); |
|
7116
|
|
- fd.append("_wpnonce", btn.getAttribute("data-nonce")); |
|
7117
|
|
- fetch(ajaxurl, { method:"POST", credentials:"same-origin", body: fd }) |
|
7118
|
|
- .then(function(r){ return r.json(); }) |
|
7119
|
|
- .then(function(j){ |
|
7120
|
|
- if (j && j.success) { |
|
7121
|
|
- out.textContent = "✓ " + (j.data && j.data.message ? j.data.message : "' . esc_js(__('OK', 'mxchat')) . '"); |
|
7122
|
|
- out.style.color = "#00a32a"; |
|
7123
|
|
- } else { |
|
7124
|
|
- out.textContent = "⚠ " + (j && j.data && j.data.message ? j.data.message : "' . esc_js(__('Failed', 'mxchat')) . '"); |
|
7125
|
|
- out.style.color = "#d63638"; |
|
7126
|
|
- } |
|
7127
|
|
- }) |
|
7128
|
|
- .catch(function(){ |
|
7129
|
|
- out.textContent = "⚠ ' . esc_js(__('Request failed', 'mxchat')) . '"; |
|
7130
|
|
- out.style.color = "#d63638"; |
|
7131
|
|
- }); |
|
7132
|
|
- }); |
|
7133
|
|
- })();</script>'; |
|
7134
|
|
- |
|
7135
|
|
- echo '</div>'; |
|
7136
|
|
-} |
|
7137
|
|
- |
|
7138
|
5756
|
// Voyage API Key |
|
7139
|
5757
|
public function voyage_api_key_callback() { |
|
7140
|
5758
|
$apiKey = isset($this->options['voyage_api_key']) ? esc_attr($this->options['voyage_api_key']) : ''; |
|
7141
|
5759
|
$nonce = wp_create_nonce('mxchat_autosave_nonce'); |
|
@@ -7344,15 +5962,60 @@ |
|
7344
|
5962
|
} |
|
7345
|
5963
|
|
|
7346
|
5964
|
|
|
7347
|
5965
|
public function mxchat_model_callback() { |
|
7348
|
|
- // Catalog refactor (plan-d14e89): single source of truth lives in |
|
7349
|
|
- // includes/class-mxchat-model-catalog.php. Dropdown groups are the |
|
7350
|
|
- // provider labels; each group maps model_id => "Label" strings. |
|
7351
|
|
- if (!class_exists('MxChat_Model_Catalog')) { |
|
7352
|
|
- require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; |
|
7353
|
|
- } |
|
7354
|
|
- $models = MxChat_Model_Catalog::settings_dropdown_groups(); |
|
5966
|
+ // Define available models grouped by provider |
|
5967
|
+ $models = array( |
|
5968
|
+ esc_html__('OpenRouter (100+ Models)', 'mxchat') => array( |
|
5969
|
+ 'openrouter' => esc_html__('OpenRouter - Select after entering API key', 'mxchat'), |
|
5970
|
+ ), |
|
5971
|
+ esc_html__('Google Gemini Models', 'mxchat') => array( |
|
5972
|
+ 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'), |
|
5973
|
+ 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'), |
|
5974
|
+ 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'), |
|
5975
|
+ 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'), |
|
5976
|
+ 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'), |
|
5977
|
+ 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'), |
|
5978
|
+ 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'), |
|
5979
|
+ 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'), |
|
5980
|
+ 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'), |
|
5981
|
+ ), |
|
5982
|
+ esc_html__('X.AI Models', 'mxchat') => array( |
|
5983
|
+ 'grok-3-beta' => esc_html__('Grok-3 (Powerful)', 'mxchat'), |
|
5984
|
+ 'grok-3-fast-beta' => esc_html__('Grok-3 Fast (High Performance)', 'mxchat'), |
|
5985
|
+ 'grok-3-mini-beta' => esc_html__('Grok-3 Mini (Affordable)', 'mxchat'), |
|
5986
|
+ 'grok-3-mini-fast-beta' => esc_html__('Grok-3 Mini Fast (Quick Response)', 'mxchat'), |
|
5987
|
+ 'grok-2' => esc_html__('Grok 2', 'mxchat'), |
|
5988
|
+ 'grok-4-0709' => esc_html__('Grok 4 (Latest Flagship)', 'mxchat'), |
|
5989
|
+ 'grok-4-1-fast-reasoning' => esc_html__('Grok 4.1 Fast (Reasoning)', 'mxchat'), |
|
5990
|
+ 'grok-4-1-fast-non-reasoning' => esc_html__('Grok 4.1 Fast (Non-Reasoning)', 'mxchat'), |
|
5991
|
+ ), |
|
5992
|
+ esc_html__('DeepSeek Models', 'mxchat') => array( |
|
5993
|
+ 'deepseek-chat' => esc_html__('DeepSeek-V3', 'mxchat'), |
|
5994
|
+ ), |
|
5995
|
+ esc_html__('Claude Models', 'mxchat') => array( |
|
5996
|
+ 'claude-opus-4-6' => esc_html__('Claude Opus 4.6 (Most Capable - Recommended)', 'mxchat'), |
|
5997
|
+ 'claude-sonnet-4-6' => esc_html__('Claude Sonnet 4.6 (Latest Sonnet - Fast & Capable)', 'mxchat'), |
|
5998
|
+ 'claude-opus-4-5' => esc_html__('Claude Opus 4.5 (Highly Capable)', 'mxchat'), |
|
5999
|
+ 'claude-sonnet-4-5-20250929' => esc_html__('Claude Sonnet 4.5 (Best for Agents & Coding)', 'mxchat'), |
|
6000
|
+ 'claude-opus-4-1-20250805' => esc_html__('Claude Opus 4.1 (Exceptional for Complex Tasks)', 'mxchat'), |
|
6001
|
+ 'claude-haiku-4-5-20251001' => esc_html__('Claude Haiku 4.5 (Fastest & Most Intelligent)', 'mxchat'), |
|
6002
|
+ 'claude-opus-4-20250514' => esc_html__('Claude 4 Opus (Complex Tasks)', 'mxchat'), |
|
6003
|
+ 'claude-sonnet-4-20250514' => esc_html__('Claude 4 Sonnet (High Performance)', 'mxchat'), |
|
6004
|
+ ), |
|
6005
|
+ esc_html__('OpenAI Models', 'mxchat') => array( |
|
6006
|
+ 'gpt-5.4' => esc_html__('GPT-5.4 (Flagship Reasoning & Coding)', 'mxchat'), |
|
6007
|
+ 'gpt-5.4-mini' => esc_html__('GPT-5.4 Mini (Fast, 400K Context)', 'mxchat'), |
|
6008
|
+ 'gpt-5.4-nano' => esc_html__('GPT-5.4 Nano (Fastest & Cheapest)', 'mxchat'), |
|
6009
|
+ 'gpt-5.3-chat-latest' => esc_html__('GPT-5.3 Chat (Conversational)', 'mxchat'), |
|
6010
|
+ 'gpt-5.2' => esc_html__('GPT-5.2 (Best General-Purpose & Agentic Model)', 'mxchat'), |
|
6011
|
+ 'gpt-5.1-chat-latest' => esc_html__('GPT-5.1 Chat Latest (Recommended)', 'mxchat'), |
|
6012
|
+ 'gpt-5.1-2025-11-13' => esc_html__('GPT-5.1 (Flagship for Coding & Agentic Tasks)', 'mxchat'), |
|
6013
|
+ 'gpt-5' => esc_html__('GPT-5 (Flagship for Coding, Reasoning & Agents)', 'mxchat'), |
|
6014
|
+ 'gpt-5-mini' => esc_html__('GPT-5 Mini (Fast and Lightweight)', 'mxchat'), |
|
6015
|
+ 'gpt-5-nano' => esc_html__('GPT-5 Nano (Fastest & Cheapest for Summarization/Classification)', 'mxchat'), |
|
6016
|
+ ), |
|
6017
|
+ ); |
|
7355
|
6018
|
|
|
7356
|
6019
|
// Retrieve the currently selected model from saved options |
|
7357
|
6020
|
$selected_model = isset($this->options['model']) ? esc_attr($this->options['model']) : 'gpt-5.1-chat-latest'; |
|
7358
|
6021
|
|
|
@@ -7461,15 +6124,15 @@ |
|
7461
|
6124
|
); |
|
7462
|
6125
|
echo '<span class="slider"></span>'; |
|
7463
|
6126
|
echo '</label>'; |
|
7464
|
6127
|
|
|
7465
|
|
- // Test button — branded .mxch-btn with inline SVG (IDs preserved for AJAX binding) |
|
7466
|
|
- echo '<div class="mxch-streaming-test-row">'; |
|
7467
|
|
- echo '<button type="button" id="mxchat-test-streaming-btn" class="mxch-btn mxch-btn-secondary">'; |
|
7468
|
|
- 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>'; |
|
6128
|
+ // Test button with better styling |
|
6129
|
+ echo '<div style="margin-top: 15px;">'; |
|
6130
|
+ echo '<button type="button" id="mxchat-test-streaming-btn" class="button button-secondary">'; |
|
6131
|
+ echo '<span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 5px;"></span>'; |
|
7469
|
6132
|
echo esc_html__('Test Streaming Compatibility', 'mxchat'); |
|
7470
|
6133
|
echo '</button>'; |
|
7471
|
|
- echo '<p id="mxchat-test-streaming-result" class="mxch-streaming-test-result"></p>'; |
|
6134
|
+ echo '<p id="mxchat-test-streaming-result" style="margin-top:10px; font-weight: bold;"></p>'; |
|
7472
|
6135
|
echo '</div>'; |
|
7473
|
6136
|
} |
|
7474
|
6137
|
|
|
7475
|
6138
|
// Web Search toggle callback |
|
@@ -7480,23 +6143,17 @@ |
|
7480
|
6143
|
|
|
7481
|
6144
|
// Get current model to determine if we should show/enable the toggle |
|
7482
|
6145
|
$current_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest'; |
|
7483
|
6146
|
|
|
7484
|
|
- // Models that DON'T support web search — OpenAI-docs-driven exception list. |
|
7485
|
|
- // Keep hardcoded; the catalog can't infer "supports web search" per-model, so any |
|
7486
|
|
- // future OpenAI model that lacks Responses-API web_search support is added here. |
|
6147
|
+ // Models that DON'T support web search (per OpenAI docs) |
|
6148
|
+ // gpt-5 with minimal reasoning is handled at API level, gpt-4.1-nano doesn't support it |
|
7487
|
6149
|
$unsupported_models = array('gpt-4.1-nano'); |
|
7488
|
6150
|
|
|
7489
|
|
- // OpenAI chat-model allowlist for the Web Search toggle, derived from the central |
|
7490
|
|
- // model catalog (class-mxchat-model-catalog.php). When a new OpenAI chat model is |
|
7491
|
|
- // added there, the Web Search toggle picks it up automatically — no edit here. |
|
7492
|
|
- if (!class_exists('MxChat_Model_Catalog')) { |
|
7493
|
|
- require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; |
|
7494
|
|
- } |
|
7495
|
|
- $chat_catalog = MxChat_Model_Catalog::chat_models(); |
|
7496
|
|
- $openai_models = (isset($chat_catalog['openai']['models']) && is_array($chat_catalog['openai']['models'])) |
|
7497
|
|
- ? array_keys($chat_catalog['openai']['models']) |
|
7498
|
|
- : array(); |
|
6151
|
+ // Check if current model is an OpenAI model that supports web search |
|
6152
|
+ $openai_models = array( |
|
6153
|
+ 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest', |
|
6154
|
+ 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano' |
|
6155
|
+ ); |
|
7499
|
6156
|
|
|
7500
|
6157
|
$is_openai = in_array($current_model, $openai_models); |
|
7501
|
6158
|
$is_supported = $is_openai && !in_array($current_model, $unsupported_models); |
|
7502
|
6159
|
|
|
@@ -7622,9 +6279,8 @@ |
|
7622
|
6279
|
} else { |
|
7623
|
6280
|
echo '<span style="color: #d63638;">⚠ ' . esc_html__('No API key for Google Gemini detected. Please enter API key in API Keys tab.', 'mxchat') . '</span>'; |
|
7624
|
6281
|
} |
|
7625
|
6282
|
echo '</p>'; |
|
7626
|
|
- |
|
7627
|
6283
|
} |
|
7628
|
6284
|
|
|
7629
|
6285
|
|
|
7630
|
6286
|
public function mxchat_top_bar_title_callback() { |
|
@@ -7859,125 +6515,8 @@ |
|
7859
|
6515
|
echo '<span class="slider"></span>'; |
|
7860
|
6516
|
echo '</label>'; |
|
7861
|
6517
|
} |
|
7862
|
6518
|
|
|
7863
|
|
-/** |
|
7864
|
|
- * Toggle for the end-of-session satisfaction rating prompt (plan-a5b006). |
|
7865
|
|
- * Default ON. The widget reads this through the localized object; the |
|
7866
|
|
- * mxchat_satisfaction_rating_enabled filter still lets developers force |
|
7867
|
|
- * the value site-wide. |
|
7868
|
|
- * |
|
7869
|
|
- * The 5 customization fields (idle/question/thanks/placeholder/saved) are |
|
7870
|
|
- * rendered inline here inside a single wrapper div whose initial display |
|
7871
|
|
- * is set server-side from the toggle value (plan-29caac). Mirrors the |
|
7872
|
|
- * auto-display chatbot pattern at mxchat_append_to_body_callback — no |
|
7873
|
|
- * DOMContentLoaded race because rows exist as direct children of this |
|
7874
|
|
- * callback's output, and the wrapper's display: none is inline at render |
|
7875
|
|
- * time so refresh shows the correct state with no flash. |
|
7876
|
|
- */ |
|
7877
|
|
-public function mxchat_satisfaction_rating_toggle_callback() { |
|
7878
|
|
- $options = $this->options; |
|
7879
|
|
- $value = isset($options['satisfaction_rating_enabled']) ? $options['satisfaction_rating_enabled'] : 'off'; |
|
7880
|
|
- $checked = ($value === 'on') ? 'checked' : ''; |
|
7881
|
|
- |
|
7882
|
|
- $idle = isset($options['satisfaction_rating_idle_seconds']) ? intval($options['satisfaction_rating_idle_seconds']) : 60; |
|
7883
|
|
- $idle = max(5, min(600, $idle)); |
|
7884
|
|
- $question = isset($options['satisfaction_rating_question']) ? $options['satisfaction_rating_question'] : ''; |
|
7885
|
|
- $thanks = isset($options['satisfaction_rating_thanks']) ? $options['satisfaction_rating_thanks'] : ''; |
|
7886
|
|
- $placeholder = isset($options['satisfaction_rating_placeholder']) ? $options['satisfaction_rating_placeholder'] : ''; |
|
7887
|
|
- $saved = isset($options['satisfaction_rating_saved']) ? $options['satisfaction_rating_saved'] : ''; |
|
7888
|
|
- |
|
7889
|
|
- echo '<div class="mxchat-autosave-section">'; |
|
7890
|
|
- |
|
7891
|
|
- echo '<label class="toggle-switch">'; |
|
7892
|
|
- echo sprintf( |
|
7893
|
|
- '<input type="checkbox" id="satisfaction_rating_enabled" name="satisfaction_rating_enabled" value="on" %s />', |
|
7894
|
|
- esc_attr($checked) |
|
7895
|
|
- ); |
|
7896
|
|
- echo '<span class="slider"></span>'; |
|
7897
|
|
- echo '</label>'; |
|
7898
|
|
- |
|
7899
|
|
- ?> |
|
7900
|
|
- <style> |
|
7901
|
|
- .mxchat-sub-options-field { margin: 12px 0; } |
|
7902
|
|
- .mxchat-sub-options-field label { display: inline-block; margin-bottom: 4px; } |
|
7903
|
|
- #satisfaction-rating-sub-options h4 { margin: 16px 0 8px; } |
|
7904
|
|
- </style> |
|
7905
|
|
- <?php |
|
7906
|
|
- |
|
7907
|
|
- $display_style = ($value === 'on') ? '' : 'display: none;'; |
|
7908
|
|
- echo '<div id="satisfaction-rating-sub-options" class="mxchat-sub-options" style="' . esc_attr($display_style) . '">'; |
|
7909
|
|
- |
|
7910
|
|
- echo '<h4>' . esc_html__('Customize the prompt (optional)', 'mxchat') . '</h4>'; |
|
7911
|
|
- |
|
7912
|
|
- echo '<div class="mxchat-sub-options-field">'; |
|
7913
|
|
- echo '<label for="satisfaction_rating_idle_seconds"><strong>' . esc_html__('Idle Timeout', 'mxchat') . '</strong></label><br />'; |
|
7914
|
|
- printf( |
|
7915
|
|
- '<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>', |
|
7916
|
|
- (int) $idle, |
|
7917
|
|
- esc_html__('seconds of user inactivity before the prompt appears (5-600)', 'mxchat') |
|
7918
|
|
- ); |
|
7919
|
|
- echo '</div>'; |
|
7920
|
|
- |
|
7921
|
|
- echo '<div class="mxchat-sub-options-field">'; |
|
7922
|
|
- echo '<label for="satisfaction_rating_question"><strong>' . esc_html__('Prompt Question', 'mxchat') . '</strong></label><br />'; |
|
7923
|
|
- printf( |
|
7924
|
|
- '<input type="text" id="satisfaction_rating_question" name="satisfaction_rating_question" value="%s" maxlength="200" class="regular-text" placeholder="%s" />', |
|
7925
|
|
- esc_attr($question), |
|
7926
|
|
- esc_attr__('Was this helpful?', 'mxchat') |
|
7927
|
|
- ); |
|
7928
|
|
- echo '<p class="description">' . esc_html__('Leave blank for the default. Shown above the thumbs up/down.', 'mxchat') . '</p>'; |
|
7929
|
|
- echo '</div>'; |
|
7930
|
|
- |
|
7931
|
|
- echo '<div class="mxchat-sub-options-field">'; |
|
7932
|
|
- echo '<label for="satisfaction_rating_thanks"><strong>' . esc_html__('Thank-You Message', 'mxchat') . '</strong></label><br />'; |
|
7933
|
|
- printf( |
|
7934
|
|
- '<input type="text" id="satisfaction_rating_thanks" name="satisfaction_rating_thanks" value="%s" maxlength="300" class="regular-text" placeholder="%s" />', |
|
7935
|
|
- esc_attr($thanks), |
|
7936
|
|
- esc_attr__('Thanks! Anything we should improve? (optional)', 'mxchat') |
|
7937
|
|
- ); |
|
7938
|
|
- echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the user clicks a thumb.', 'mxchat') . '</p>'; |
|
7939
|
|
- echo '</div>'; |
|
7940
|
|
- |
|
7941
|
|
- echo '<div class="mxchat-sub-options-field">'; |
|
7942
|
|
- echo '<label for="satisfaction_rating_placeholder"><strong>' . esc_html__('Feedback Placeholder', 'mxchat') . '</strong></label><br />'; |
|
7943
|
|
- printf( |
|
7944
|
|
- '<input type="text" id="satisfaction_rating_placeholder" name="satisfaction_rating_placeholder" value="%s" maxlength="200" class="regular-text" placeholder="%s" />', |
|
7945
|
|
- esc_attr($placeholder), |
|
7946
|
|
- esc_attr__('Tell us what could be better…', 'mxchat') |
|
7947
|
|
- ); |
|
7948
|
|
- echo '<p class="description">' . esc_html__('Leave blank for the default. Placeholder text inside the feedback textarea.', 'mxchat') . '</p>'; |
|
7949
|
|
- echo '</div>'; |
|
7950
|
|
- |
|
7951
|
|
- echo '<div class="mxchat-sub-options-field">'; |
|
7952
|
|
- echo '<label for="satisfaction_rating_saved"><strong>' . esc_html__('Saved Confirmation', 'mxchat') . '</strong></label><br />'; |
|
7953
|
|
- printf( |
|
7954
|
|
- '<input type="text" id="satisfaction_rating_saved" name="satisfaction_rating_saved" value="%s" maxlength="200" class="regular-text" placeholder="%s" />', |
|
7955
|
|
- esc_attr($saved), |
|
7956
|
|
- esc_attr__('Thanks for the feedback.', 'mxchat') |
|
7957
|
|
- ); |
|
7958
|
|
- echo '<p class="description">' . esc_html__('Leave blank for the default. Shown after the feedback is sent.', 'mxchat') . '</p>'; |
|
7959
|
|
- echo '</div>'; |
|
7960
|
|
- |
|
7961
|
|
- echo '</div>'; // #satisfaction-rating-sub-options |
|
7962
|
|
- echo '</div>'; // .mxchat-autosave-section |
|
7963
|
|
- |
|
7964
|
|
- ?> |
|
7965
|
|
- <script> |
|
7966
|
|
- (function() { |
|
7967
|
|
- document.addEventListener('DOMContentLoaded', function() { |
|
7968
|
|
- var toggle = document.getElementById('satisfaction_rating_enabled'); |
|
7969
|
|
- var subOptions = document.getElementById('satisfaction-rating-sub-options'); |
|
7970
|
|
- if (!toggle || !subOptions) return; |
|
7971
|
|
- toggle.addEventListener('change', function() { |
|
7972
|
|
- subOptions.style.display = toggle.checked ? '' : 'none'; |
|
7973
|
|
- }); |
|
7974
|
|
- }); |
|
7975
|
|
- })(); |
|
7976
|
|
- </script> |
|
7977
|
|
- <?php |
|
7978
|
|
-} |
|
7979
|
|
- |
|
7980
|
6519
|
public function mxchat_privacy_toggle_callback() { |
|
7981
|
6520
|
// Load from mxchat_options array |
|
7982
|
6521
|
$options = get_option('mxchat_options', []); |
|
7983
|
6522
|
|
|
@@ -8060,26 +6599,8 @@ |
|
8060
|
6599
|
echo '<span class="slider"></span>'; |
|
8061
|
6600
|
echo '</label>'; |
|
8062
|
6601
|
} |
|
8063
|
6602
|
|
|
8064
|
|
-public function mxchat_print_button_toggle_callback() { |
|
8065
|
|
- // Load from mxchat_options array |
|
8066
|
|
- $options = get_option('mxchat_options', []); |
|
8067
|
|
- |
|
8068
|
|
- // Default ON — the option was previously unexposed and the button always showed. |
|
8069
|
|
- $print_button_enabled = isset($options['print_button_enabled']) ? $options['print_button_enabled'] : 'on'; |
|
8070
|
|
- $checked = ($print_button_enabled === 'on') ? 'checked' : ''; |
|
8071
|
|
- |
|
8072
|
|
- // Output the toggle switch |
|
8073
|
|
- echo '<label class="toggle-switch">'; |
|
8074
|
|
- echo sprintf( |
|
8075
|
|
- '<input type="checkbox" id="print_button_enabled" name="print_button_enabled" value="on" %s />', |
|
8076
|
|
- esc_attr($checked) |
|
8077
|
|
- ); |
|
8078
|
|
- echo '<span class="slider"></span>'; |
|
8079
|
|
- echo '</label>'; |
|
8080
|
|
-} |
|
8081
|
|
- |
|
8082
|
6603
|
public function mxchat_popular_question_1_callback() { |
|
8083
|
6604
|
// Load the full plugin options array |
|
8084
|
6605
|
$all_options = get_option('mxchat_options', []); |
|
8085
|
6606
|
|
|
@@ -8663,20 +7184,8 @@ |
|
8663
|
7184
|
); |
|
8664
|
7185
|
echo '</div>'; |
|
8665
|
7186
|
} |
|
8666
|
7187
|
|
|
8667
|
|
-/** |
|
8668
|
|
- * Add body class on the Onboarding wizard page so CSS-only chrome surgery |
|
8669
|
|
- * (collapsing the WP admin sidebar) only applies on this page. |
|
8670
|
|
- * Plan: plan-mxchat-20260527-905439. |
|
8671
|
|
- */ |
|
8672
|
|
-public function mxchat_add_onboarding_body_class($classes) { |
|
8673
|
|
- if (isset($_GET['page']) && $_GET['page'] === 'mxchat-onboarding') { |
|
8674
|
|
- $classes .= ' mxchat-onboarding-focused'; |
|
8675
|
|
- } |
|
8676
|
|
- return $classes; |
|
8677
|
|
-} |
|
8678
|
|
- |
|
8679
|
7188
|
public function mxchat_enqueue_admin_assets() { |
|
8680
|
7189
|
// Get plugin version |
|
8681
|
7190
|
$version = MXCHAT_VERSION; |
|
8682
|
7191
|
|
|
@@ -8778,38 +7287,8 @@ |
|
8778
|
7287
|
wp_enqueue_style('mxchat-content-css', $plugin_url . 'css/admin-content.css', array('mxchat-admin-sidebar-css'), $version); |
|
8779
|
7288
|
// Load content page JavaScript |
|
8780
|
7289
|
wp_enqueue_script('mxchat-content-js', $plugin_url . 'js/mxchat-content.js', array('jquery'), $version, true); |
|
8781
|
7290
|
break; |
|
8782
|
|
- |
|
8783
|
|
- case 'mxchat-api-access': |
|
8784
|
|
- // Shared sidebar shell (CSS + JS for tab switching / mobile menu / copy buttons). |
|
8785
|
|
- wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version); |
|
8786
|
|
- wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true); |
|
8787
|
|
- wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array( |
|
8788
|
|
- 'copied' => __('Copied', 'mxchat'), |
|
8789
|
|
- )); |
|
8790
|
|
- break; |
|
8791
|
|
- |
|
8792
|
|
- case 'mxchat-max': |
|
8793
|
|
- case 'mxchat-onboarding': |
|
8794
|
|
- // Onboarding page — uses the shared admin shell PLUS the wizard |
|
8795
|
|
- // overlay (plan-905439). admin-onboarding-wizard.css scopes the |
|
8796
|
|
- // WP-chrome surgery to body.mxchat-onboarding-focused so it only |
|
8797
|
|
- // applies on THIS page. The body class is added below via the |
|
8798
|
|
- // admin_body_class filter. |
|
8799
|
|
- wp_enqueue_style('mxchat-admin-sidebar-css', $plugin_url . 'css/admin-sidebar.css', array(), $version); |
|
8800
|
|
- wp_enqueue_script('mxchat-admin-sidebar-js', $plugin_url . 'js/admin-sidebar.js', array(), $version, true); |
|
8801
|
|
- wp_localize_script('mxchat-admin-sidebar-js', 'MxChatAdminSidebarI18n', array( |
|
8802
|
|
- 'copied' => __('Copied', 'mxchat'), |
|
8803
|
|
- )); |
|
8804
|
|
- // admin-style.css provides the .mxchat-instructions-modal-* classes |
|
8805
|
|
- // the new Behavior step's "View Sample Instructions" modal needs. |
|
8806
|
|
- // Enqueued AFTER admin-sidebar.css but BEFORE the wizard overlay |
|
8807
|
|
- // so the wizard's own rules win where they collide. plan-a2e4d6. |
|
8808
|
|
- wp_enqueue_style('mxchat-admin-style-css', $plugin_url . 'css/admin-style.css', array('mxchat-admin-sidebar-css'), $version); |
|
8809
|
|
- 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); |
|
8810
|
|
- wp_enqueue_script('mxchat-admin-onboarding-wizard-js', $plugin_url . 'js/admin-onboarding-wizard.js', array(), $version, true); |
|
8811
|
|
- break; |
|
8812
|
7291
|
default: |
|
8813
|
7292
|
wp_enqueue_script( |
|
8814
|
7293
|
'mxchat-test-streaming-js', |
|
8815
|
7294
|
$plugin_url . 'js/mxchat-test-streaming.js', |
|
@@ -8922,16 +7401,8 @@ |
|
8922
|
7401
|
|
|
8923
|
7402
|
// Localize main admin script with base data |
|
8924
|
7403
|
wp_localize_script('mxchat-admin-js', 'mxchatAdmin', $base_data); |
|
8925
|
7404
|
|
|
8926
|
|
- // Canonical chat-model catalog for the modal picker grid |
|
8927
|
|
- // (plan-d14e89). Adding a model in class-mxchat-model-catalog.php |
|
8928
|
|
- // automatically appears here. |
|
8929
|
|
- if (!class_exists('MxChat_Model_Catalog')) { |
|
8930
|
|
- require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; |
|
8931
|
|
- } |
|
8932
|
|
- wp_localize_script('mxchat-admin-js', 'mxchatChatModelCatalog', MxChat_Model_Catalog::js_picker_shape()); |
|
8933
|
|
- |
|
8934
|
7405
|
// Page-specific localizations |
|
8935
|
7406
|
$this->localize_page_specific_scripts($current_page); |
|
8936
|
7407
|
} |
|
8937
|
7408
|
private function localize_page_specific_scripts($current_page) { |
|
@@ -8943,22 +7414,17 @@ |
|
8943
|
7414
|
'nonce' => wp_create_nonce('mxchat_status_nonce') |
|
8944
|
7415
|
)); |
|
8945
|
7416
|
|
|
8946
|
7417
|
// Content selector localization |
|
8947
|
|
- $mxchat_options_for_selector = get_option('mxchat_options', array()); |
|
8948
|
|
- $acf_pdf_extract_default = !empty($mxchat_options_for_selector['acf_pdf_extract_default']); |
|
8949
|
7418
|
wp_localize_script('mxchat-content-selector-js', 'mxchatSelector', array( |
|
8950
|
7419
|
'ajaxurl' => admin_url('admin-ajax.php'), |
|
8951
|
7420
|
'nonce' => wp_create_nonce('mxchat_content_selector_nonce'), |
|
8952
|
|
- 'acfPdfExtractDefault' => $acf_pdf_extract_default ? 1 : 0, |
|
8953
|
7421
|
'i18n' => array( |
|
8954
|
7422
|
'searchPlaceholder' => __('Search posts and pages...', 'mxchat'), |
|
8955
|
7423
|
'selectAll' => __('Select All', 'mxchat'), |
|
8956
|
7424
|
'process' => __('Process Selected', 'mxchat'), |
|
8957
|
7425
|
'cancel' => __('Cancel', 'mxchat'), |
|
8958
|
|
- 'noResults' => __('No content found.', 'mxchat'), |
|
8959
|
|
- 'extractingPdfs' => __('extracting %d PDF(s)...', 'mxchat'), |
|
8960
|
|
- 'pdfExtractedSuffix' => __(' (%d PDF(s) extracted)', 'mxchat') |
|
7426
|
+ 'noResults' => __('No content found.', 'mxchat') |
|
8961
|
7427
|
) |
|
8962
|
7428
|
)); |
|
8963
|
7429
|
|
|
8964
|
7430
|
wp_localize_script('mxchat-knowledge-processing', 'mxchatAdmin', array( |
|
@@ -9192,35 +7658,8 @@ |
|
9192
|
7658
|
if (isset($input['citation_links_toggle'])) { |
|
9193
|
7659
|
$new_input['citation_links_toggle'] = $input['citation_links_toggle'] === 'on' ? 'on' : 'off'; |
|
9194
|
7660
|
} |
|
9195
|
7661
|
|
|
9196
|
|
- // Satisfaction rating prompt — defaults ON when unchecked (so first save |
|
9197
|
|
- // doesn't accidentally disable it). The form posts 'on' when checked; |
|
9198
|
|
- // unchecked checkboxes don't post a value at all, so we infer 'off' only |
|
9199
|
|
- // when the autosave/PHP request explicitly clears it via empty string. |
|
9200
|
|
- if (array_key_exists('satisfaction_rating_enabled', $input)) { |
|
9201
|
|
- $new_input['satisfaction_rating_enabled'] = $input['satisfaction_rating_enabled'] === 'on' ? 'on' : 'off'; |
|
9202
|
|
- } |
|
9203
|
|
- |
|
9204
|
|
- // Satisfaction rating customization (plan-141a12). Idle is clamped 5-600; |
|
9205
|
|
- // the 4 text strings are sanitized + capped server-side. Blank values are |
|
9206
|
|
- // preserved so the integrator falls back to translated defaults. |
|
9207
|
|
- if (array_key_exists('satisfaction_rating_idle_seconds', $input)) { |
|
9208
|
|
- $new_input['satisfaction_rating_idle_seconds'] = max(5, min(600, intval($input['satisfaction_rating_idle_seconds']))); |
|
9209
|
|
- } |
|
9210
|
|
- if (array_key_exists('satisfaction_rating_question', $input)) { |
|
9211
|
|
- $new_input['satisfaction_rating_question'] = mb_substr(sanitize_text_field($input['satisfaction_rating_question']), 0, 200); |
|
9212
|
|
- } |
|
9213
|
|
- if (array_key_exists('satisfaction_rating_thanks', $input)) { |
|
9214
|
|
- $new_input['satisfaction_rating_thanks'] = mb_substr(sanitize_text_field($input['satisfaction_rating_thanks']), 0, 300); |
|
9215
|
|
- } |
|
9216
|
|
- if (array_key_exists('satisfaction_rating_placeholder', $input)) { |
|
9217
|
|
- $new_input['satisfaction_rating_placeholder'] = mb_substr(sanitize_text_field($input['satisfaction_rating_placeholder']), 0, 200); |
|
9218
|
|
- } |
|
9219
|
|
- if (array_key_exists('satisfaction_rating_saved', $input)) { |
|
9220
|
|
- $new_input['satisfaction_rating_saved'] = mb_substr(sanitize_text_field($input['satisfaction_rating_saved']), 0, 200); |
|
9221
|
|
- } |
|
9222
|
|
- |
|
9223
|
7662
|
if (isset($input['top_bar_title'])) { |
|
9224
|
7663
|
$new_input['top_bar_title'] = sanitize_text_field($input['top_bar_title']); |
|
9225
|
7664
|
} |
|
9226
|
7665
|
|
|
@@ -9272,13 +7711,9 @@ |
|
9272
|
7711
|
|
|
9273
|
7712
|
// Sanitize limit |
|
9274
|
7713
|
if (isset($settings['limit'])) { |
|
9275
|
7714
|
$limit = sanitize_text_field($settings['limit']); |
|
9276
|
|
- // Accept presets, 'unlimited', the '__custom__' sentinel, OR any positive |
|
9277
|
|
- // integer (custom value) — mirrors the global branch (plan-2c02ea). Without |
|
9278
|
|
- // the custom path the per-role custom input was dropped and reset to the role |
|
9279
|
|
- // default on every save (plan-7e23e7). |
|
9280
|
|
- if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) { |
|
7715
|
+ if (in_array($limit, $allowed_limits, true)) { |
|
9281
|
7716
|
$new_input['rate_limits'][$role_id]['limit'] = $limit; |
|
9282
|
7717
|
} else { |
|
9283
|
7718
|
$new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default |
|
9284
|
7719
|
} |
|
@@ -9283,13 +7718,8 @@ |
|
9283
|
7718
|
$new_input['rate_limits'][$role_id]['limit'] = ($role_id === 'logged_out') ? '10' : '100'; // Default |
|
9284
|
7719
|
} |
|
9285
|
7720
|
} |
|
9286
|
7721
|
|
|
9287
|
|
- // Preserve the per-role custom value (mirrors the global branch's limit_custom). |
|
9288
|
|
- if (isset($settings['limit_custom'])) { |
|
9289
|
|
- $new_input['rate_limits'][$role_id]['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $settings['limit_custom']); |
|
9290
|
|
- } |
|
9291
|
|
- |
|
9292
|
7722
|
// Sanitize timeframe |
|
9293
|
7723
|
if (isset($settings['timeframe'])) { |
|
9294
|
7724
|
$timeframe = sanitize_text_field($settings['timeframe']); |
|
9295
|
7725
|
if (in_array($timeframe, $allowed_timeframes, true)) { |
|
@@ -9305,46 +7735,8 @@ |
|
9305
|
7735
|
} |
|
9306
|
7736
|
} |
|
9307
|
7737
|
} |
|
9308
|
7738
|
|
|
9309
|
|
-// Handle the whole-chatbot global rate limit (plan-mxchat-20260603-2c02ea). |
|
9310
|
|
-// Mirrors the per-role block above, adapted to the single global shape. Without |
|
9311
|
|
-// this branch the whitelist-rebuild dropped rate_limits_global entirely on every |
|
9312
|
|
-// save, so the global cap silently fell back to its 'unlimited' default. |
|
9313
|
|
-// MUST accept arbitrary positive integers so the custom-value path (d55f65) is not regressed. |
|
9314
|
|
-if (isset($input['rate_limits_global']) && is_array($input['rate_limits_global'])) { |
|
9315
|
|
- $g = $input['rate_limits_global']; |
|
9316
|
|
- $allowed_limits = array('1', '3', '5', '10', '15', '20', '50', '100', 'unlimited'); |
|
9317
|
|
- $allowed_timeframes = array('hourly', 'daily', 'weekly', 'monthly'); |
|
9318
|
|
- $global_out = array(); |
|
9319
|
|
- |
|
9320
|
|
- if (isset($g['limit'])) { |
|
9321
|
|
- $limit = sanitize_text_field($g['limit']); |
|
9322
|
|
- // Accept presets, 'unlimited', the '__custom__' sentinel (resolved by the |
|
9323
|
|
- // autosave handler / renderer), OR any positive integer (custom value). |
|
9324
|
|
- if (in_array($limit, $allowed_limits, true) || $limit === '__custom__' || (ctype_digit($limit) && (int) $limit >= 1)) { |
|
9325
|
|
- $global_out['limit'] = $limit; |
|
9326
|
|
- } else { |
|
9327
|
|
- $global_out['limit'] = 'unlimited'; |
|
9328
|
|
- } |
|
9329
|
|
- } |
|
9330
|
|
- |
|
9331
|
|
- if (isset($g['limit_custom'])) { |
|
9332
|
|
- $global_out['limit_custom'] = preg_replace('/[^0-9]/', '', (string) $g['limit_custom']); |
|
9333
|
|
- } |
|
9334
|
|
- |
|
9335
|
|
- if (isset($g['timeframe'])) { |
|
9336
|
|
- $timeframe = sanitize_text_field($g['timeframe']); |
|
9337
|
|
- $global_out['timeframe'] = in_array($timeframe, $allowed_timeframes, true) ? $timeframe : 'daily'; |
|
9338
|
|
- } |
|
9339
|
|
- |
|
9340
|
|
- if (isset($g['message'])) { |
|
9341
|
|
- $global_out['message'] = sanitize_textarea_field($g['message']); |
|
9342
|
|
- } |
|
9343
|
|
- |
|
9344
|
|
- $new_input['rate_limits_global'] = $global_out; |
|
9345
|
|
-} |
|
9346
|
|
- |
|
9347
|
7739
|
if (isset($input['pre_chat_message'])) { |
|
9348
|
7740
|
$new_input['pre_chat_message'] = sanitize_textarea_field($input['pre_chat_message']); |
|
9349
|
7741
|
} |
|
9350
|
7742
|
|
|
@@ -9353,14 +7745,15 @@ |
|
9353
|
7745
|
} |
|
9354
|
7746
|
|
|
9355
|
7747
|
// Add to your sanitize function |
|
9356
|
7748
|
if (isset($input['embedding_model'])) { |
|
9357
|
|
- // Catalog refactor (plan-d14e89): allowlist derived from the canonical |
|
9358
|
|
- // catalog in includes/class-mxchat-model-catalog.php. |
|
9359
|
|
- if (!class_exists('MxChat_Model_Catalog')) { |
|
9360
|
|
- require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; |
|
9361
|
|
- } |
|
9362
|
|
- $allowed_models = MxChat_Model_Catalog::embedding_model_ids(); |
|
7749
|
+ $allowed_models = array( |
|
7750
|
+ 'text-embedding-ada-002', |
|
7751
|
+ 'text-embedding-3-small', |
|
7752
|
+ 'text-embedding-3-large', |
|
7753
|
+ 'voyage-3-large', |
|
7754
|
+ 'gemini-embedding-001' |
|
7755
|
+ ); |
|
9363
|
7756
|
if (in_array($input['embedding_model'], $allowed_models)) { |
|
9364
|
7757
|
$new_input['embedding_model'] = sanitize_text_field($input['embedding_model']); |
|
9365
|
7758
|
} |
|
9366
|
7759
|
} |
|
@@ -9368,12 +7761,46 @@ |
|
9368
|
7761
|
if (isset($input['model'])) { |
|
9369
|
7762
|
if ($input['model'] === 'openrouter') { |
|
9370
|
7763
|
$new_input['model'] = 'openrouter'; |
|
9371
|
7764
|
} else { |
|
9372
|
|
- if (!class_exists('MxChat_Model_Catalog')) { |
|
9373
|
|
- require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; |
|
9374
|
|
- } |
|
9375
|
|
- $allowed_models = MxChat_Model_Catalog::chat_model_ids(); |
|
7765
|
+ $allowed_models = array( |
|
7766
|
+ 'gemini-3-pro-preview', |
|
7767
|
+ 'gemini-3-flash-preview', |
|
7768
|
+ 'gemini-2.5-pro', |
|
7769
|
+ 'gemini-2.5-flash', |
|
7770
|
+ 'gemini-2.5-flash-lite', |
|
7771
|
+ 'gemini-2.0-flash', |
|
7772
|
+ 'gemini-2.0-flash-lite', |
|
7773
|
+ 'gemini-1.5-pro', |
|
7774
|
+ 'gemini-1.5-flash', |
|
7775
|
+ 'grok-4-0709', |
|
7776
|
+ 'grok-4-1-fast-reasoning', |
|
7777
|
+ 'grok-4-1-fast-non-reasoning', |
|
7778
|
+ 'grok-3-beta', |
|
7779
|
+ 'grok-3-fast-beta', |
|
7780
|
+ 'grok-3-mini-beta', |
|
7781
|
+ 'grok-3-mini-fast-beta', |
|
7782
|
+ 'grok-2', |
|
7783
|
+ 'deepseek-chat', |
|
7784
|
+ 'claude-opus-4-6', |
|
7785
|
+ 'claude-sonnet-4-6', |
|
7786
|
+ 'claude-opus-4-5', |
|
7787
|
+ 'claude-sonnet-4-5-20250929', |
|
7788
|
+ 'claude-opus-4-1-20250805', |
|
7789
|
+ 'claude-haiku-4-5-20251001', |
|
7790
|
+ 'claude-opus-4-20250514', |
|
7791
|
+ 'claude-sonnet-4-20250514', |
|
7792
|
+ 'gpt-5.2', |
|
7793
|
+ 'gpt-5.1-chat-latest', |
|
7794
|
+ 'gpt-5.1-2025-11-13', |
|
7795
|
+ 'gpt-5', |
|
7796
|
+ 'gpt-5-mini', |
|
7797
|
+ 'gpt-5-nano', |
|
7798
|
+ 'gpt-5.4', |
|
7799
|
+ 'gpt-5.4-mini', |
|
7800
|
+ 'gpt-5.4-nano', |
|
7801
|
+ 'gpt-5.3-chat-latest', |
|
7802
|
+ ); |
|
9376
|
7803
|
|
|
9377
|
7804
|
if (in_array($input['model'], $allowed_models)) { |
|
9378
|
7805
|
$new_input['model'] = sanitize_text_field($input['model']); |
|
9379
|
7806
|
} else { |
|
@@ -9396,37 +7823,10 @@ |
|
9396
|
7823
|
if (isset($input['openrouter_api_key'])) { |
|
9397
|
7824
|
$new_input['openrouter_api_key'] = sanitize_text_field($input['openrouter_api_key']); |
|
9398
|
7825
|
} |
|
9399
|
7826
|
|
|
9400
|
|
- // Custom (OpenAI-compatible) Provider — Ollama, LM Studio, vLLM, Azure OpenAI, etc. |
|
9401
|
|
- if (isset($input['custom_provider_base_url'])) { |
|
9402
|
|
- $new_input['custom_provider_base_url'] = esc_url_raw(rtrim(trim((string) $input['custom_provider_base_url']), '/')); |
|
9403
|
|
- } |
|
9404
|
|
- if (isset($input['custom_provider_api_key'])) { |
|
9405
|
|
- $new_input['custom_provider_api_key'] = sanitize_text_field($input['custom_provider_api_key']); |
|
9406
|
|
- } |
|
9407
|
|
- if (isset($input['custom_provider_model'])) { |
|
9408
|
|
- $new_input['custom_provider_model'] = sanitize_text_field($input['custom_provider_model']); |
|
9409
|
|
- } |
|
9410
|
|
- if (isset($input['custom_provider_auth_scheme'])) { |
|
9411
|
|
- $scheme = sanitize_text_field($input['custom_provider_auth_scheme']); |
|
9412
|
|
- $new_input['custom_provider_auth_scheme'] = in_array($scheme, array('bearer', 'api-key'), true) ? $scheme : 'bearer'; |
|
9413
|
|
- } |
|
9414
|
|
- if (isset($input['custom_provider_for_embeddings'])) { |
|
9415
|
|
- $new_input['custom_provider_for_embeddings'] = ($input['custom_provider_for_embeddings'] === 'on') ? 'on' : 'off'; |
|
9416
|
|
- } |
|
9417
|
|
- if (isset($input['custom_provider_for_images'])) { |
|
9418
|
|
- $new_input['custom_provider_for_images'] = ($input['custom_provider_for_images'] === 'on') ? 'on' : 'off'; |
|
9419
|
|
- } |
|
9420
|
|
- if (isset($input['custom_provider_embedding_model'])) { |
|
9421
|
|
- $new_input['custom_provider_embedding_model'] = sanitize_text_field($input['custom_provider_embedding_model']); |
|
9422
|
|
- } |
|
9423
|
|
- if (isset($input['custom_provider_api_version'])) { |
|
9424
|
|
- $new_input['custom_provider_api_version'] = sanitize_text_field($input['custom_provider_api_version']); |
|
9425
|
|
- } |
|
9426
|
7827
|
|
|
9427
|
7828
|
|
|
9428
|
|
- |
|
9429
|
7829
|
if (isset($input['woocommerce_consumer_key'])) { |
|
9430
|
7830
|
$new_input['woocommerce_consumer_key'] = sanitize_text_field($input['woocommerce_consumer_key']); |
|
9431
|
7831
|
} |
|
9432
|
7832
|
|
|
@@ -9448,14 +7848,8 @@ |
|
9448
|
7848
|
if (isset($input['chat_persistence_toggle'])) { |
|
9449
|
7849
|
$new_input['chat_persistence_toggle'] = $input['chat_persistence_toggle'] === 'on' ? 'on' : 'off'; |
|
9450
|
7850
|
} |
|
9451
|
7851
|
|
|
9452
|
|
- // No else clause: an absent key stays absent, so the front-end default ('on') applies |
|
9453
|
|
- // and the rebuild never strips a saved 'off' (autosave passes the full options array back through here). |
|
9454
|
|
- if (isset($input['print_button_enabled'])) { |
|
9455
|
|
- $new_input['print_button_enabled'] = $input['print_button_enabled'] === 'on' ? 'on' : 'off'; |
|
9456
|
|
- } |
|
9457
|
|
- |
|
9458
|
7852
|
if (isset($input['popular_question_1'])) { |
|
9459
|
7853
|
$new_input['popular_question_1'] = sanitize_text_field($input['popular_question_1']); |
|
9460
|
7854
|
} |
|
9461
|
7855
|
|
|
@@ -9614,12 +8008,8 @@ |
|
9614
|
8008
|
} |
|
9615
|
8009
|
if (isset($input['content_image_model'])) { |
|
9616
|
8010
|
$new_input['content_image_model'] = sanitize_text_field($input['content_image_model']); |
|
9617
|
8011
|
} |
|
9618
|
|
- if (isset($input['content_image_quality'])) { |
|
9619
|
|
- $q = sanitize_text_field($input['content_image_quality']); |
|
9620
|
|
- $new_input['content_image_quality'] = in_array($q, array('auto', 'low', 'medium', 'high'), true) ? $q : 'auto'; |
|
9621
|
|
- } |
|
9622
|
8012
|
if (isset($input['content_enable_images'])) { |
|
9623
|
8013
|
$new_input['content_enable_images'] = ($input['content_enable_images'] === 'on') ? 'on' : 'off'; |
|
9624
|
8014
|
} |
|
9625
|
8015
|
if (isset($input['content_use_placeholders'])) { |
|
@@ -9630,11 +8020,8 @@ |
|
9630
|
8020
|
} |
|
9631
|
8021
|
if (isset($input['content_tool_use'])) { |
|
9632
|
8022
|
$new_input['content_tool_use'] = ($input['content_tool_use'] === 'on') ? 'on' : 'off'; |
|
9633
|
8023
|
} |
|
9634
|
|
- if (isset($input['content_image_count'])) { |
|
9635
|
|
- $new_input['content_image_count'] = (string) max(1, min(5, (int) $input['content_image_count'])); |
|
9636
|
|
- } |
|
9637
|
8024
|
|
|
9638
|
8025
|
// SEO Optimize toggle fields |
|
9639
|
8026
|
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) { |
|
9640
|
8027
|
if (isset($input[$seo_key])) { |
|
@@ -9644,9 +8031,9 @@ |
|
9644
|
8031
|
|
|
9645
|
8032
|
// Preserve content generator settings when saving from main settings page |
|
9646
|
8033
|
// (where content fields are not in the form submission) |
|
9647
|
8034
|
$existing = get_option('mxchat_options', array()); |
|
9648
|
|
- 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) { |
|
8035
|
+ 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) { |
|
9649
|
8036
|
if (!isset($new_input[$key]) && isset($existing[$key])) { |
|
9650
|
8037
|
$new_input[$key] = $existing[$key]; |
|
9651
|
8038
|
} |
|
9652
|
8039
|
} |
|
@@ -10233,9 +8620,8 @@ |
|
10233
|
8620
|
if ($embedding_dimensions !== 1536) { |
|
10234
|
8621
|
//error_log('[MXCHAT-EMBED] Warning: Unexpected Gemini embedding dimensions: ' . $embedding_dimensions); |
|
10235
|
8622
|
} |
|
10236
|
8623
|
|
|
10237
|
|
- MxChat_Utils::stamp_active_embedding_model($selected_model); |
|
10238
|
8624
|
return $response_data['embedding']['values']; |
|
10239
|
8625
|
} else { |
|
10240
|
8626
|
//error_log('[MXCHAT-EMBED] Error: No embedding found in Gemini response'); |
|
10241
|
8627
|
//error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data))); |
|
@@ -10261,9 +8647,8 @@ |
|
10261
|
8647
|
($selected_model === 'voyage-3-large' && $embedding_dimensions !== 2048)) { |
|
10262
|
8648
|
//error_log('[MXCHAT-EMBED] Warning: Unexpected embedding dimensions'); |
|
10263
|
8649
|
} |
|
10264
|
8650
|
|
|
10265
|
|
- MxChat_Utils::stamp_active_embedding_model($selected_model); |
|
10266
|
8651
|
return $response_data['data'][0]['embedding']; |
|
10267
|
8652
|
} else { |
|
10268
|
8653
|
//error_log('[MXCHAT-EMBED] Error: No embedding found in response'); |
|
10269
|
8654
|
//error_log('[MXCHAT-EMBED] Response structure: ' . wp_json_encode(array_keys($response_data))); |